Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
d8ee37833e | |||
e31bc93074 | |||
f711f8919d | |||
|
b27add5767 | ||
1934915ae8 | |||
d54b3f9a2c | |||
f8fc729486 | |||
a41bbdb745 | |||
d4c3476031 | |||
fb3ee62962 | |||
384a4e436c | |||
a22bf9caf1 | |||
4e533b8044 | |||
a003e176f5 | |||
ec33a7ad74 | |||
2970af4873 | |||
9abb66ad1d | |||
6ef6df535d | |||
|
f0798b3f3a | ||
93ba19a1fa | |||
fb33b2a74e | |||
8c36d42e06 | |||
709f24b5df | |||
f7b4861b1d | |||
0c3a736977 | |||
f75b0c7313 | |||
bfa5f0df1d | |||
be88bc29f5 | |||
4780228c95 | |||
2f34e19636 | |||
3ec9bdfb01 | |||
|
cab7d8badd | ||
fae73e71bc | |||
74aac3fdcb | |||
f50f8d0f93 | |||
1970ac19e4 | |||
532ee3f1a3 | |||
490f235c6d | |||
466d6d40d3 | |||
bf2687a211 | |||
d652bf41f5 | |||
618b5bc861 | |||
8659907d62 | |||
|
c66595115f | ||
6ac85912c1 | |||
d18e68b138 | |||
43f2dd5452 | |||
72413b0bac | |||
fb5f21f1f3 | |||
8f1ae3a8a4 | |||
a6163db5b8 | |||
9d2ceb8cba | |||
3940cd8975 | |||
7ad6343e6f | |||
2cd50ff38c | |||
f855fe9c7a | |||
f1557d104d | |||
7f3b3a859d | |||
7718c533eb | |||
0208a2a134 | |||
238d9a51af | |||
bf88399b58 | |||
c97388fdae | |||
e5dcf8defb | |||
badaedc443 | |||
a41be6d27c | |||
d97b58159c | |||
6c93c8be56 | |||
d7f2c453a9 | |||
d9f253d65c | |||
13efc1a646 | |||
c923817c06 | |||
35e6aae345 | |||
9893c8a95a | |||
77eac66d01 | |||
4763760a1c | |||
941052b33b | |||
7c12ca465a | |||
6d8a8d80f5 | |||
e767de2c83 | |||
480cae12cf | |||
eb36d45c35 | |||
d0b775ab21 | |||
77690dbb57 | |||
02ca54f141 | |||
82f7d662c7 | |||
4720b1cd17 | |||
0192f32905 | |||
ecd02424e1 | |||
1853fec85a | |||
7ee4e251e8 | |||
96026b88ab | |||
1721146c7d | |||
3417f50e11 | |||
bb4c641a02 | |||
46494b54c9 | |||
b09f0e07ad | |||
adbd4c7233 | |||
0c24236da9 | |||
92cd70430b | |||
dd1b5006cb | |||
9f702e5071 | |||
c7ea09b6a1 | |||
8b098731d2 | |||
d2d378ec11 | |||
841bf6a7e4 | |||
18af3227cc | |||
598dec4e32 | |||
56104bbc0f | |||
6c23c42f80 | |||
5ac3ab9292 | |||
62d8a1019d | |||
29d953c14d | |||
2af13ef626 | |||
|
0d4379946b | ||
|
e9f3ac9193 | ||
a40b7d3c07 | |||
8538bb0521 | |||
13d913db38 | |||
085e57a07a | |||
20ba409039 | |||
0c9bfb6dd8 | |||
8c1455b030 | |||
690f697ee0 |
@ -3,3 +3,9 @@ tests
|
|||||||
disk*
|
disk*
|
||||||
qemu.sh
|
qemu.sh
|
||||||
**/*.qcow2
|
**/*.qcow2
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
images
|
||||||
|
examples/build
|
||||||
|
e2e
|
||||||
|
**/*_test.go
|
||||||
|
376
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
name: Tests and Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
tags: [ "v*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Setup dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-tests-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-tests-
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || make tests
|
||||||
|
|
||||||
|
templates-tests:
|
||||||
|
name: Test Templates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
image:
|
||||||
|
- ubuntu
|
||||||
|
- debian
|
||||||
|
- kalilinux
|
||||||
|
- alpine
|
||||||
|
- centos
|
||||||
|
- quay.io/centos/centos:stream9
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Setup dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-tests-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-tests-
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || IMAGE=${{ matrix.image }} make test-templates
|
||||||
|
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
name: End to end Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
image:
|
||||||
|
- alpine:3.17
|
||||||
|
- ubuntu:20.04
|
||||||
|
- ubuntu:22.04
|
||||||
|
- debian:10
|
||||||
|
- debian:11
|
||||||
|
- centos:8
|
||||||
|
- quay.io/centos/centos:stream9
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Setup dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system ovmf
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-e2e-tests-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-tests-
|
||||||
|
|
||||||
|
- name: Run end-to-end tests
|
||||||
|
run: E2E_IMAGES=${{ matrix.image }} make e2e
|
||||||
|
|
||||||
|
docs-up-to-date:
|
||||||
|
name: Docs up to date
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-tests-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-tests-
|
||||||
|
|
||||||
|
- name: Check if docs are up to date
|
||||||
|
run: make docs-up-to-date
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
|
||||||
|
- name: Ensure all files were well formatted
|
||||||
|
run: make check-fmt
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v4
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build Snapshot
|
||||||
|
run: make build-snapshot
|
||||||
|
|
||||||
|
- name: Release Snapshot
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
|
||||||
|
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
run: make release-snapshot
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
name: Build Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-build-image-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-image-
|
||||||
|
|
||||||
|
- name: Build Docker images
|
||||||
|
run: make docker-build
|
||||||
|
|
||||||
|
- name: Push Docker images
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
run: make docker-push
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release Binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
|
needs:
|
||||||
|
- tests
|
||||||
|
- templates-tests
|
||||||
|
- docs-up-to-date
|
||||||
|
- build
|
||||||
|
- e2e-tests
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v4
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: Release binaries
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
|
||||||
|
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
run: make release
|
||||||
|
|
||||||
|
release-image:
|
||||||
|
name: Release Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
|
needs:
|
||||||
|
- tests
|
||||||
|
- docs-up-to-date
|
||||||
|
- build-image
|
||||||
|
- e2e-tests
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# fetching all tags is required for the Makefile to compute the right version
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "1.20"
|
||||||
|
|
||||||
|
- name: Set up QEMU dependency
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Share cache with other actions
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-build-image-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-image-
|
||||||
|
|
||||||
|
- name: Build Docker images
|
||||||
|
run: make docker-build
|
||||||
|
|
||||||
|
- name: Release Docker images
|
||||||
|
run: make docker-push
|
20
.github/workflows/docs.yaml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [ "v*" ]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Build and deploy mkdocs site
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
make build-docs
|
||||||
|
sudo chown -R ${UID}:${UID} docs
|
||||||
|
make deploy-docs
|
||||||
|
|
14
.gitignore
vendored
@ -1,6 +1,20 @@
|
|||||||
.idea
|
.idea
|
||||||
tests
|
tests
|
||||||
|
scratch
|
||||||
*.qcow2
|
*.qcow2
|
||||||
|
*.vmdk
|
||||||
|
*.vdi
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
bin/
|
||||||
dist/
|
dist/
|
||||||
|
images
|
||||||
|
/d2vm
|
||||||
|
/examples/build
|
||||||
|
/examples/full/demo-magic
|
||||||
|
/examples/full/inside
|
||||||
.goreleaser.yaml
|
.goreleaser.yaml
|
||||||
|
docs/build
|
||||||
|
docs-src
|
||||||
|
/completions
|
||||||
|
/cmd/d2vm/run/sparsecat-linux-*
|
||||||
|
81
.goreleaser.yaml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
project_name: d2vm
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
- make completions
|
||||||
|
builds:
|
||||||
|
- main: ./cmd/d2vm
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X "go.linka.cloud/d2vm.Image={{.Env.IMAGE}}" -X "go.linka.cloud/d2vm.Version={{.Env.VERSION}}" -X "go.linka.cloud/d2vm.BuildDate={{.CommitDate}}"
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
signs:
|
||||||
|
- artifacts: all
|
||||||
|
stdin: '{{ .Env.GPG_PASSWORD }}'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Env.VERSION }}"
|
||||||
|
release:
|
||||||
|
prerelease: auto
|
||||||
|
extra_files:
|
||||||
|
- glob: LICENCE
|
||||||
|
- glob: pgp.pub
|
||||||
|
archives:
|
||||||
|
- name_template: '{{ .ProjectName }}_{{ .Env.VERSION }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
files:
|
||||||
|
- LICENCE
|
||||||
|
- README.md
|
||||||
|
- completions/*
|
||||||
|
brews:
|
||||||
|
- name: d2vm
|
||||||
|
tap:
|
||||||
|
owner: linka-cloud
|
||||||
|
name: homebrew-tap
|
||||||
|
folder: Formula
|
||||||
|
homepage: https://github.com/linka-cloud/d2vm
|
||||||
|
description: Build Virtual Machine Image from Dockerfile or Docker image
|
||||||
|
license: Apache License 2.0
|
||||||
|
test: |
|
||||||
|
system "#{bin}/d2vm --version"
|
||||||
|
dependencies:
|
||||||
|
- name: go
|
||||||
|
type: optional
|
||||||
|
- name: git
|
||||||
|
install: |-
|
||||||
|
bin.install "d2vm"
|
||||||
|
bash_completion.install "completions/d2vm.bash" => "d2vm"
|
||||||
|
zsh_completion.install "completions/d2vm.zsh" => "_d2vm"
|
||||||
|
fish_completion.install "completions/d2vm.fish"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
- '^tests:'
|
||||||
|
- '^actions:'
|
||||||
|
- '^Makefile:'
|
||||||
|
- '^chore:'
|
||||||
|
- '^goreleaser:'
|
33
Dockerfile
@ -1,4 +1,18 @@
|
|||||||
FROM golang as builder
|
# Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
FROM golang:1.20 as builder
|
||||||
|
|
||||||
WORKDIR /d2vm
|
WORKDIR /d2vm
|
||||||
|
|
||||||
@ -9,21 +23,26 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN make build
|
RUN make .build
|
||||||
|
|
||||||
FROM ubuntu
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
util-linux \
|
util-linux \
|
||||||
|
udev \
|
||||||
|
parted \
|
||||||
kpartx \
|
kpartx \
|
||||||
e2fsprogs \
|
e2fsprogs \
|
||||||
xfsprogs \
|
dosfstools \
|
||||||
mount \
|
mount \
|
||||||
tar \
|
tar \
|
||||||
extlinux \
|
"$([ "$(uname -m)" = "x86_64" ] && echo extlinux)" \
|
||||||
uuid-runtime \
|
cryptsetup-bin \
|
||||||
qemu-utils
|
qemu-utils && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||||
|
|
||||||
|
121
Makefile
@ -16,20 +16,42 @@ MODULE = go.linka.cloud/d2vm
|
|||||||
|
|
||||||
REPOSITORY = linkacloud
|
REPOSITORY = linkacloud
|
||||||
|
|
||||||
|
TAG = $(shell git diff --quiet && git describe --tags --exact-match 2> /dev/null)
|
||||||
VERSION_SUFFIX = $(shell git diff --quiet || echo "-dev")
|
VERSION_SUFFIX = $(shell git diff --quiet || echo "-dev")
|
||||||
VERSION = $(shell git describe --tags --exact-match 2> /dev/null || echo "`git describe --tags $$(git rev-list --tags --max-count=1) 2> /dev/null || echo v0.0.0`-`git rev-parse --short HEAD`")$(VERSION_SUFFIX)
|
VERSION = $(shell git describe --tags --exact-match 2> /dev/null || echo "`git describe --tags $$(git rev-list --tags --max-count=1) 2> /dev/null || echo v0.0.0`-`git rev-parse --short HEAD`")$(VERSION_SUFFIX)
|
||||||
show-version:
|
show-version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
|
GORELEASER_VERSION := v1.10.1
|
||||||
|
GORELEASER_URL := https://github.com/goreleaser/goreleaser/releases/download/$(GORELEASER_VERSION)/goreleaser_Linux_x86_64.tar.gz
|
||||||
|
|
||||||
|
BIN := $(PWD)/bin
|
||||||
|
export PATH := $(BIN):$(PATH)
|
||||||
|
|
||||||
|
CLI_REFERENCE_PATH := docs/content/reference
|
||||||
|
|
||||||
|
bin:
|
||||||
|
@mkdir -p $(BIN)
|
||||||
|
@curl -sL $(GORELEASER_URL) | tar -C $(BIN) -xz goreleaser
|
||||||
|
|
||||||
|
clean-bin:
|
||||||
|
@rm -rf $(BIN)
|
||||||
|
|
||||||
DOCKER_IMAGE := linkacloud/d2vm
|
DOCKER_IMAGE := linkacloud/d2vm
|
||||||
|
|
||||||
docker: docker-build docker-push
|
docker: docker-build docker-push
|
||||||
|
|
||||||
docker-push:
|
docker-push:
|
||||||
@docker image push -a $(DOCKER_IMAGE)
|
@docker image push $(DOCKER_IMAGE):$(VERSION)
|
||||||
|
ifneq ($(TAG),)
|
||||||
|
@docker image push $(DOCKER_IMAGE):latest
|
||||||
|
endif
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
@docker image build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest .
|
@docker image build -t $(DOCKER_IMAGE):$(VERSION) .
|
||||||
|
ifneq ($(TAG),)
|
||||||
|
@docker image tag $(DOCKER_IMAGE):$(TAG) $(DOCKER_IMAGE):latest
|
||||||
|
endif
|
||||||
|
|
||||||
docker-run:
|
docker-run:
|
||||||
@docker run --rm -i -t \
|
@docker run --rm -i -t \
|
||||||
@ -39,5 +61,96 @@ docker-run:
|
|||||||
-w /build \
|
-w /build \
|
||||||
$(DOCKER_IMAGE) bash
|
$(DOCKER_IMAGE) bash
|
||||||
|
|
||||||
build:
|
.PHONY: tests
|
||||||
@go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
tests:
|
||||||
|
@go generate ./...
|
||||||
|
@go list .| xargs go test -exec sudo -count=1 -timeout 60m -v -skip TestConfig
|
||||||
|
|
||||||
|
.PHONY: test-templates
|
||||||
|
test-templates:
|
||||||
|
@go generate ./...
|
||||||
|
@go test -exec sudo -count=1 -timeout 60m -v -run TestConfig/$(IMAGE)
|
||||||
|
|
||||||
|
e2e: docker-build .build
|
||||||
|
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e -args -images $(E2E_IMAGES)
|
||||||
|
|
||||||
|
docs-up-to-date:
|
||||||
|
@$(MAKE) cli-docs
|
||||||
|
@git diff --quiet -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md' || (git --no-pager diff -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md'; echo "Please regenerate the documentation with 'make cli-docs'"; exit 1)
|
||||||
|
|
||||||
|
check-fmt:
|
||||||
|
@[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@go list ./...|grep -v scratch|GOOS=linux xargs go vet
|
||||||
|
|
||||||
|
build-dev: docker-build .build
|
||||||
|
|
||||||
|
install: docker-build
|
||||||
|
@go generate ./...
|
||||||
|
@go install -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
||||||
|
|
||||||
|
.build:
|
||||||
|
@go generate ./...
|
||||||
|
@CGO_ENABLED=0 go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
||||||
|
|
||||||
|
.PHONY: build-snapshot
|
||||||
|
build-snapshot: bin
|
||||||
|
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser build --snapshot --rm-dist --parallelism 8
|
||||||
|
|
||||||
|
.PHONY: release-snapshot
|
||||||
|
release-snapshot: bin
|
||||||
|
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser release --snapshot --rm-dist --skip-announce --skip-publish --parallelism 8
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: bin
|
||||||
|
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser build --rm-dist --parallelism 8
|
||||||
|
|
||||||
|
.PHONY: release
|
||||||
|
release: bin
|
||||||
|
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser release --rm-dist --parallelism 8
|
||||||
|
|
||||||
|
.PHONY: completions
|
||||||
|
completions: .build
|
||||||
|
@rm -rf completions
|
||||||
|
@mkdir -p completions
|
||||||
|
@for shell in bash zsh fish powershell; do \
|
||||||
|
./d2vm completion $$shell > completions/d2vm.$$shell; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: examples
|
||||||
|
examples: build-dev
|
||||||
|
@mkdir -p examples/build
|
||||||
|
@for f in $$(find examples -maxdepth 1 -type f -name '*Dockerfile'); do \
|
||||||
|
echo "Building $$f"; \
|
||||||
|
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
|
||||||
|
done
|
||||||
|
@echo "Building examples/full/Dockerfile"
|
||||||
|
@./d2vm build -o examples/build/full.qcow2 --build-arg=USER=adphi --build-arg=PASSWORD=adphi examples/full --force
|
||||||
|
|
||||||
|
cli-docs: .build
|
||||||
|
@rm -rf $(CLI_REFERENCE_PATH)
|
||||||
|
@./d2vm docs $(CLI_REFERENCE_PATH)
|
||||||
|
|
||||||
|
serve-docs:
|
||||||
|
@docker run --rm -i -t --user=$(UID) -p 8000:8000 -v $(PWD):/docs linkacloud/mkdocs-material serve -f /docs/docs/mkdocs.yml -a 0.0.0.0:8000
|
||||||
|
|
||||||
|
.PHONY: build-docs
|
||||||
|
build-docs: clean-docs cli-docs
|
||||||
|
@docker run --rm -v $(PWD):/docs linkacloud/mkdocs-material build -f /docs/docs/mkdocs.yml -d build
|
||||||
|
|
||||||
|
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
GITHUB_PAGES_BRANCH := gh-pages
|
||||||
|
|
||||||
|
deploy-docs:
|
||||||
|
@git branch -D gh-pages &> /dev/null || true
|
||||||
|
@git checkout -b $(GITHUB_PAGES_BRANCH)
|
||||||
|
@rm .gitignore && mv docs docs-src && mv docs-src/build docs && rm -rf docs-src
|
||||||
|
@git add . && git commit -m "build docs" && git push origin --force $(GITHUB_PAGES_BRANCH)
|
||||||
|
@git checkout $(GIT_BRANCH)
|
||||||
|
|
||||||
|
docs: cli-docs build-docs deploy-docs
|
||||||
|
|
||||||
|
clean-docs:
|
||||||
|
@rm -rf docs/build
|
||||||
|
241
README.md
@ -1,5 +1,10 @@
|
|||||||
|
|
||||||
# d2vm (Docker to Virtual Machine)
|
# d2vm (Docker to Virtual Machine)
|
||||||
|
|
||||||
|
[![Language: Go](https://img.shields.io/badge/lang-Go-6ad7e5.svg?style=flat-square&logo=go)](https://golang.org/)
|
||||||
|
[![Go Reference](https://pkg.go.dev/badge/go.linka.cloud/d2vm.svg)](https://pkg.go.dev/go.linka.cloud/d2vm)
|
||||||
|
[![Chat](https://img.shields.io/badge/chat-matrix-blue.svg?style=flat-square&logo=matrix)](https://matrix.to/#/#d2vm:linka.cloud)
|
||||||
|
|
||||||
*Build virtual machine image from Docker images*
|
*Build virtual machine image from Docker images*
|
||||||
|
|
||||||
The project is heavily inspired by the [article](https://iximiuz.com/en/posts/from-docker-container-to-bootable-linux-disk-image/) and the work done by [iximiuz](https://github.com/iximiuz) on [docker-to-linux](https://github.com/iximiuz/docker-to-linux).
|
The project is heavily inspired by the [article](https://iximiuz.com/en/posts/from-docker-container-to-bootable-linux-disk-image/) and the work done by [iximiuz](https://github.com/iximiuz) on [docker-to-linux](https://github.com/iximiuz/docker-to-linux).
|
||||||
@ -8,70 +13,142 @@ Many thanks to him.
|
|||||||
|
|
||||||
**Status**: *alpha*
|
**Status**: *alpha*
|
||||||
|
|
||||||
|
[![asciicast](https://asciinema.org/a/520132.svg)](https://asciinema.org/a/520132)
|
||||||
|
|
||||||
## Supported Environments:
|
## Supported Environments:
|
||||||
|
|
||||||
**Only Linux is supported.**
|
**Only building Linux Virtual Machine images is supported.**
|
||||||
|
|
||||||
If you want to run it on **OSX** or **Windows** (the last one is totally untested) you can do it using Docker:
|
Starting from v0.1.0, **d2vm** automatically run build and convert commands inside Docker when not running on linux
|
||||||
|
or when running without *root* privileges.
|
||||||
|
|
||||||
```bash
|
*Note: windows should be working, but is totally untested.*
|
||||||
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported VM Linux distributions:
|
## Supported VM Linux distributions:
|
||||||
|
|
||||||
Working and tested:
|
Working and tested:
|
||||||
|
|
||||||
- [x] Ubuntu
|
- [x] Ubuntu (18.04+)
|
||||||
- [x] Debian
|
Luks support is available only on Ubuntu 20.04+
|
||||||
|
- [x] Debian (stretch+)
|
||||||
|
Luks support is available only on Debian buster+
|
||||||
- [x] Alpine
|
- [x] Alpine
|
||||||
|
- [x] CentOS (8+)
|
||||||
Need fix:
|
|
||||||
|
|
||||||
- [ ] CentOS
|
|
||||||
|
|
||||||
Unsupported:
|
Unsupported:
|
||||||
|
|
||||||
- [ ] RHEL
|
- [ ] RHEL
|
||||||
|
|
||||||
The program use the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
||||||
if the file is missing, the build cannot succeed.
|
if the file is missing, the build cannot succeed.
|
||||||
|
|
||||||
Obviously, **Distroless** images are not supported.
|
Obviously, **Distroless** images are not supported.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### osx
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- [QEMU](https://www.qemu.org/download/#macos) (optional)
|
||||||
|
- [VirtualBox](https://www.virtualbox.org/wiki/Downloads) (optional)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- util-linux
|
||||||
|
- udev
|
||||||
|
- parted
|
||||||
|
- e2fsprogs
|
||||||
|
- dosfstools (when using fat32)
|
||||||
|
- mount
|
||||||
|
- tar
|
||||||
|
- extlinux (when using syslinux)
|
||||||
|
- qemu-utils
|
||||||
|
- cryptsetup (when using LUKS)
|
||||||
|
- [QEMU](https://www.qemu.org/download/#linux) (optional)
|
||||||
|
- [VirtualBox](https://www.virtualbox.org/wiki/Linux_Downloads) (optional)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
#### With Docker
|
||||||
|
|
||||||
|
*Note: this will only work if both the source context (and Dockerfile) and the output directory are somewhere inside
|
||||||
|
the directory where you run the command.*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull linkacloud/d2vm:latest
|
||||||
|
alias d2vm="docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --privileged -v \$PWD:/d2vm -w /d2vm linkacloud/d2vm:latest"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
which d2vm
|
||||||
|
|
||||||
|
d2vm: aliased to docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --privileged -v $PWD:/d2vm -w /d2vm linkacloud/d2vm:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With Homebrew
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install linka-cloud/tap/d2vm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### From release
|
||||||
|
|
||||||
|
Download the latest release for your platform from the [release page](https://github.com/linka-cloud/d2vm/releases/latest).
|
||||||
|
|
||||||
|
Extract the tarball, then move the extracted *d2vm* binary to somewhere in your `$PATH` (`/usr/local/bin` for most users).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION=$(git ls-remote --tags https://github.com/linka-cloud/d2vm |cut -d'/' -f 3|tail -n 1)
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$([ "$(uname -m)" = "x86_64" ] && echo "amd64" || echo "arm64")
|
||||||
|
curl -sL "https://github.com/linka-cloud/d2vm/releases/download/${VERSION}/d2vm_${VERSION}_${OS}_${ARCH}.tar.gz" | tar -xvz d2vm
|
||||||
|
sudo mv d2vm /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### From source
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/linka-cloud/d2vm && cd d2vm
|
git clone https://github.com/linka-cloud/d2vm && cd d2vm
|
||||||
```
|
```
|
||||||
|
|
||||||
Install using the Go tool chain:
|
Install using the *make*, *docker* and the Go tool chain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
The *d2vm* binary is installed in the `$GOBIN` directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install ./cmd/d2vm
|
|
||||||
which d2vm
|
which d2vm
|
||||||
```
|
|
||||||
```
|
|
||||||
# Should be install in the $GOBIN directory
|
|
||||||
/go/bin/d2vm
|
/go/bin/d2vm
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use an alias to the **docker** image:
|
### Generate shell completion
|
||||||
|
|
||||||
|
The *d2vm* program supports shell completion for *bash*, *zsh* and *fish*.
|
||||||
|
|
||||||
|
It can be enabled by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
source <(d2vm completion $(basename $SHELL))
|
||||||
which d2vm
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or you can install the completion file in the shell completion directory by following the instructions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
d2vm completion $(basename $SHELL) --help
|
||||||
```
|
```
|
||||||
d2vm: aliased to docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm
|
|
||||||
```
|
|
||||||
|
|
||||||
### Converting an existing Docker Image to VM image:
|
### Converting an existing Docker Image to VM image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
b2vm convert --help
|
d2vm convert --help
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
Convert Docker image to vm image
|
Convert Docker image to vm image
|
||||||
@ -80,14 +157,28 @@ Usage:
|
|||||||
d2vm convert [docker image] [flags]
|
d2vm convert [docker image] [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-d, --debug Enable Debug output
|
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
||||||
-f, --force Override output qcow2 image
|
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
|
||||||
-h, --help help for convert
|
--boot-size uint Size of the boot partition in MB (default 100)
|
||||||
-o, --output string The output image (default "disk0.qcow2")
|
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
|
||||||
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
|
--force Override output qcow2 image
|
||||||
-p, --password string The Root user password (default "root")
|
-h, --help help for convert
|
||||||
--pull Always pull docker image
|
--keep-cache Keep the images after the build
|
||||||
-s, --size string The output image size (default "10G")
|
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
|
||||||
|
--network-manager string Network manager to use for the image: none, netplan, ifupdown
|
||||||
|
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
|
||||||
|
-p, --password string Optional root user password
|
||||||
|
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
|
||||||
|
--pull Always pull docker image
|
||||||
|
--push Push the container disk image to the registry
|
||||||
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
--split-boot Split the boot partition from the root partition
|
||||||
|
-t, --tag string Container disk Docker image tag
|
||||||
|
|
||||||
|
Global Flags:
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -97,27 +188,27 @@ Create an image based on the **ubuntu** official image:
|
|||||||
sudo d2vm convert ubuntu -o ubuntu.qcow2 -p MyP4Ssw0rd
|
sudo d2vm convert ubuntu -o ubuntu.qcow2 -p MyP4Ssw0rd
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
INFO[0000] pulling image ubuntu
|
Pulling image ubuntu
|
||||||
INFO[0001] inspecting image ubuntu
|
Inspecting image ubuntu
|
||||||
INFO[0002] docker image based on Ubuntu
|
No network manager specified, using distribution defaults: netplan
|
||||||
INFO[0002] building kernel enabled image
|
Docker image based on Ubuntu 22.04.1 LTS (Jammy Jellyfish)
|
||||||
INFO[0038] creating root file system archive
|
Building kernel enabled image
|
||||||
INFO[0040] creating vm image
|
Creating vm image
|
||||||
INFO[0040] creating raw image
|
Creating raw image
|
||||||
INFO[0040] mounting raw image
|
Mounting raw image
|
||||||
INFO[0040] creating raw image file system
|
Creating raw image file system
|
||||||
INFO[0040] copying rootfs to raw image
|
Copying rootfs to raw image
|
||||||
INFO[0041] setting up rootfs
|
Setting up rootfs
|
||||||
INFO[0041] installing linux kernel
|
Installing linux kernel
|
||||||
INFO[0042] unmounting raw image
|
Unmounting raw image
|
||||||
INFO[0042] writing MBR
|
Writing MBR
|
||||||
INFO[0042] converting to qcow2
|
Converting to qcow2
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now run your ubuntu image using the created `ubuntu.qcow2` image with **qemu**:
|
You can now run your ubuntu image using the created `ubuntu.qcow2` image with **qemu**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./qemu.sh ununtu.qcow2
|
d2vm run qemu ubuntu.qcow2
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
SeaBIOS (version 1.13.0-1ubuntu1.1)
|
SeaBIOS (version 1.13.0-1ubuntu1.1)
|
||||||
@ -189,7 +280,7 @@ applicable law.
|
|||||||
root@localhost:~#
|
root@localhost:~#
|
||||||
```
|
```
|
||||||
|
|
||||||
Type `poweroff` to shutdown the vm.
|
Type `poweroff` to shut down the vm.
|
||||||
|
|
||||||
### Building a VM Image from a Dockerfile
|
### Building a VM Image from a Dockerfile
|
||||||
|
|
||||||
@ -205,12 +296,10 @@ cd examples
|
|||||||
FROM ubuntu
|
FROM ubuntu
|
||||||
|
|
||||||
RUN apt update && apt install -y openssh-server && \
|
RUN apt update && apt install -y openssh-server && \
|
||||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When building the vm image, *d2vm* will create a root password, so there is no need to configure it now.
|
|
||||||
|
|
||||||
Build the vm image:
|
Build the vm image:
|
||||||
|
|
||||||
The *build* command take most of its flags and arguments from the *docker build* command.
|
The *build* command take most of its flags and arguments from the *docker build* command.
|
||||||
@ -226,15 +315,30 @@ Usage:
|
|||||||
d2vm build [context directory] [flags]
|
d2vm build [context directory] [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--build-arg stringArray Set build-time variables
|
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
||||||
-d, --debug Enable Debug output
|
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
|
||||||
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile') (default "Dockerfile")
|
--boot-size uint Size of the boot partition in MB (default 100)
|
||||||
--force Override output image
|
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
|
||||||
-h, --help help for build
|
--build-arg stringArray Set build-time variables
|
||||||
-o, --output string The output image (default "disk0.qcow2")
|
-f, --file string Name of the Dockerfile
|
||||||
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
|
--force Override output qcow2 image
|
||||||
-p, --password string Root user password (default "root")
|
-h, --help help for build
|
||||||
-s, --size string The output image size (default "10G")
|
--keep-cache Keep the images after the build
|
||||||
|
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
|
||||||
|
--network-manager string Network manager to use for the image: none, netplan, ifupdown
|
||||||
|
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
|
||||||
|
-p, --password string Optional root user password
|
||||||
|
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
|
||||||
|
--pull Always pull docker image
|
||||||
|
--push Push the container disk image to the registry
|
||||||
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
--split-boot Split the boot partition from the root partition
|
||||||
|
-t, --tag string Container disk Docker image tag
|
||||||
|
|
||||||
|
Global Flags:
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -245,9 +349,16 @@ sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.qcow2 .
|
|||||||
Or if you want to create a VirtualBox image:
|
Or if you want to create a VirtualBox image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -O vdi -o ubuntu.vdi .
|
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.vdi .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### KubeVirt Container Disk Images
|
||||||
|
|
||||||
|
Using the `--tag` flag with the `build` and `convert` commands, you can create a
|
||||||
|
[Container Disk Image](https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk) for [KubeVirt](https://kubevirt.io/).
|
||||||
|
|
||||||
|
The `--push` flag will push the image to the registry.
|
||||||
|
|
||||||
### Complete example
|
### Complete example
|
||||||
|
|
||||||
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
|
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
|
||||||
@ -261,4 +372,8 @@ You can find the Dockerfiles used to install the Kernel in the [templates](templ
|
|||||||
|
|
||||||
- [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ?
|
- [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ?
|
||||||
- [ ] Inject Image `ENV` variables into `.bashrc` or other service environment file ?
|
- [ ] Inject Image `ENV` variables into `.bashrc` or other service environment file ?
|
||||||
- [ ] Use image layers to create *rootfs* instead of container ?
|
- [x] Use image layers to create *rootfs* instead of container ?
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
|
||||||
|
The *run* commands are adapted from [linuxkit](https://github.com/docker/linuxkit).
|
||||||
|
43
bootloader.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bootloaderProviders = map[string]BootloaderProvider{}
|
||||||
|
|
||||||
|
func RegisterBootloaderProvider(provider BootloaderProvider) {
|
||||||
|
bootloaderProviders[provider.Name()] = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func BootloaderByName(name string) (BootloaderProvider, error) {
|
||||||
|
if p, ok := bootloaderProviders[name]; ok {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("bootloader provider %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootloaderProvider interface {
|
||||||
|
New(c Config, r OSRelease, arch string) (Bootloader, error)
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bootloader interface {
|
||||||
|
Validate(fs BootFS) error
|
||||||
|
Setup(ctx context.Context, dev, root, cmdline string) error
|
||||||
|
}
|
437
builder.go
@ -15,16 +15,15 @@
|
|||||||
package d2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
exec2 "os/exec"
|
exec2 "os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
@ -42,70 +41,67 @@ ff02::1 ip6-allnodes
|
|||||||
ff02::2 ip6-allrouters
|
ff02::2 ip6-allrouters
|
||||||
ff02::3 ip6-allhosts
|
ff02::3 ip6-allhosts
|
||||||
`
|
`
|
||||||
syslinuxCfgUbuntu = `DEFAULT linux
|
perm os.FileMode = 0644
|
||||||
SAY Now booting the kernel from SYSLINUX...
|
|
||||||
LABEL linux
|
|
||||||
KERNEL /boot/vmlinuz
|
|
||||||
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
|
|
||||||
`
|
|
||||||
syslinuxCfgDebian = `DEFAULT linux
|
|
||||||
SAY Now booting the kernel from SYSLINUX...
|
|
||||||
LABEL linux
|
|
||||||
KERNEL /vmlinuz
|
|
||||||
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
|
|
||||||
`
|
|
||||||
syslinuxCfgAlpine = `DEFAULT linux
|
|
||||||
SAY Now booting the kernel from SYSLINUX...
|
|
||||||
LABEL linux
|
|
||||||
KERNEL /boot/vmlinuz-virt
|
|
||||||
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200
|
|
||||||
`
|
|
||||||
syslinuxCfgCentOS = `DEFAULT linux
|
|
||||||
SAY Now booting the kernel from SYSLINUX...
|
|
||||||
LABEL linux
|
|
||||||
KERNEL /boot/vmlinuz
|
|
||||||
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
|
|
||||||
`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vhd", "vhdx", "vmdk"}
|
||||||
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
|
|
||||||
|
|
||||||
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
|
type Builder interface {
|
||||||
|
Build(ctx context.Context) (err error)
|
||||||
mbrPaths = []string{
|
Close() error
|
||||||
// debian path
|
}
|
||||||
"/usr/lib/syslinux/mbr/mbr.bin",
|
|
||||||
// ubuntu path
|
|
||||||
"/usr/lib/EXTLINUX/mbr.bin",
|
|
||||||
// alpine path
|
|
||||||
"/usr/share/syslinux/mbr.bin",
|
|
||||||
// centos path
|
|
||||||
"/usr/share/syslinux/mbr.bin",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
osRelease OSRelease
|
osRelease OSRelease
|
||||||
|
config Config
|
||||||
|
bootloader Bootloader
|
||||||
|
|
||||||
src string
|
src string
|
||||||
|
img *image
|
||||||
diskRaw string
|
diskRaw string
|
||||||
diskOut string
|
diskOut string
|
||||||
format string
|
format string
|
||||||
|
|
||||||
size int64
|
size uint64
|
||||||
mntPoint string
|
mntPoint string
|
||||||
|
|
||||||
mbrPath string
|
splitBoot bool
|
||||||
|
bootSize uint64
|
||||||
|
bootFS BootFS
|
||||||
|
|
||||||
loDevice string
|
loDevice string
|
||||||
loPart string
|
bootPart string
|
||||||
diskUUD string
|
rootPart string
|
||||||
|
cryptPart string
|
||||||
|
cryptRoot string
|
||||||
|
mappedCryptRoot string
|
||||||
|
bootUUID string
|
||||||
|
rootUUID string
|
||||||
|
cryptUUID string
|
||||||
|
|
||||||
|
luksPassword string
|
||||||
|
|
||||||
|
cmdLineExtra string
|
||||||
|
arch string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, format string) (*builder, error) {
|
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootFS BootFS, bootSize uint64, luksPassword string, bootLoader string, platform string) (Builder, error) {
|
||||||
if err := checkDependencies(); err != nil {
|
var arch string
|
||||||
return nil, err
|
switch platform {
|
||||||
|
case "linux/amd64":
|
||||||
|
arch = "x86_64"
|
||||||
|
case "linux/arm64", "linux/aarch64":
|
||||||
|
arch = "arm64"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
|
||||||
|
}
|
||||||
|
if luksPassword != "" {
|
||||||
|
if !splitBoot {
|
||||||
|
return nil, fmt.Errorf("luks encryption requires split boot")
|
||||||
|
}
|
||||||
|
if !osRelease.SupportsLUKS() {
|
||||||
|
return nil, fmt.Errorf("luks encryption not supported on %s %s", osRelease.ID, osRelease.VersionID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
f := strings.ToLower(format)
|
f := strings.ToLower(format)
|
||||||
valid := false
|
valid := false
|
||||||
@ -117,41 +113,91 @@ func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, form
|
|||||||
if !valid {
|
if !valid {
|
||||||
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
|
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
|
||||||
}
|
}
|
||||||
|
if f == "vhd" {
|
||||||
|
f = "vpc"
|
||||||
|
}
|
||||||
|
|
||||||
mbrBin := ""
|
if splitBoot && bootSize < 50 {
|
||||||
for _, v := range mbrPaths {
|
return nil, fmt.Errorf("boot partition size must be at least 50MiB")
|
||||||
if _, err := os.Stat(v); err == nil {
|
|
||||||
mbrBin = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if mbrBin == "" {
|
|
||||||
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
|
if splitBoot && bootSize >= size {
|
||||||
|
return nil, fmt.Errorf("boot partition size must be less than the disk size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootLoader == "" {
|
||||||
|
bootLoader = "syslinux"
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := osRelease.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitBoot {
|
||||||
|
config.Kernel = strings.TrimPrefix(config.Kernel, "/boot")
|
||||||
|
config.Initrd = strings.TrimPrefix(config.Initrd, "/boot")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootFS == "" {
|
||||||
|
bootFS = BootFSExt4
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bootFS.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blp, err := BootloaderByName(bootLoader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bl, err := blp.New(config, osRelease, arch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bl.Validate(bootFS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 10 * int64(datasize.GB)
|
size = 10 * uint64(datasize.GB)
|
||||||
}
|
}
|
||||||
if disk == "" {
|
if disk == "" {
|
||||||
disk = "disk0"
|
disk = "disk0"
|
||||||
}
|
}
|
||||||
i, err := os.Stat(src)
|
img, err := NewImage(ctx, imgTag, workdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if i.Size() > size {
|
// i, err := os.Stat(imgTar)
|
||||||
s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
|
// if err != nil {
|
||||||
logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
|
// return nil, err
|
||||||
size = int64(s)
|
// }
|
||||||
}
|
// if i.Size() > size {
|
||||||
|
// s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
|
||||||
|
// logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
|
||||||
|
// size = int64(s)
|
||||||
|
// }
|
||||||
b := &builder{
|
b := &builder{
|
||||||
osRelease: osRelease,
|
osRelease: osRelease,
|
||||||
src: src,
|
config: config,
|
||||||
diskRaw: filepath.Join(workdir, disk+".raw"),
|
bootloader: bl,
|
||||||
diskOut: filepath.Join(workdir, disk+".qcow2"),
|
img: img,
|
||||||
format: f,
|
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
|
||||||
size: size,
|
diskOut: filepath.Join(workdir, disk+"."+format),
|
||||||
mbrPath: mbrBin,
|
format: f,
|
||||||
mntPoint: filepath.Join(workdir, "/mnt"),
|
size: size,
|
||||||
|
mntPoint: filepath.Join(workdir, "/mnt"),
|
||||||
|
cmdLineExtra: cmdLineExtra,
|
||||||
|
splitBoot: splitBoot,
|
||||||
|
bootSize: bootSize,
|
||||||
|
bootFS: bootFS,
|
||||||
|
luksPassword: luksPassword,
|
||||||
|
arch: arch,
|
||||||
|
}
|
||||||
|
if err := b.checkDependencies(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
|
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -187,15 +233,12 @@ func (b *builder) Build(ctx context.Context) (err error) {
|
|||||||
if err = b.setupRootFS(ctx); err != nil {
|
if err = b.setupRootFS(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.installKernel(ctx); err != nil {
|
if err = b.installBootloader(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.unmountImg(ctx); err != nil {
|
if err = b.unmountImg(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.setupMBR(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = b.convert2Img(ctx); err != nil {
|
if err = b.convert2Img(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -217,19 +260,22 @@ func (b *builder) makeImg(ctx context.Context) error {
|
|||||||
if err := block(b.diskRaw, b.size); err != nil {
|
if err := block(b.diskRaw, b.size); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c := exec.CommandContext(ctx, "fdisk", b.diskRaw)
|
|
||||||
var i bytes.Buffer
|
var args []string
|
||||||
for _, v := range fdiskCmds {
|
if b.splitBoot {
|
||||||
if _, err := i.Write([]byte(v + "\n")); err != nil {
|
args = []string{"-s", b.diskRaw,
|
||||||
return err
|
"mklabel", "msdos", "mkpart", "primary", "1Mib", fmt.Sprintf("%dMib", b.bootSize),
|
||||||
|
"mkpart", "primary", fmt.Sprintf("%dMib", b.bootSize), "100%",
|
||||||
|
"set", "1", "boot", "on",
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
args = []string{"-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"}
|
||||||
}
|
}
|
||||||
var e bytes.Buffer
|
|
||||||
c.Stdin = &i
|
if err := exec.Run(ctx, "parted", args...); err != nil {
|
||||||
c.Stderr = &e
|
return err
|
||||||
if err := c.Run(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", err, e.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,12 +289,62 @@ func (b *builder) mountImg(ctx context.Context) error {
|
|||||||
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
|
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
|
b.bootPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
|
||||||
logrus.Infof("creating raw image file system")
|
b.rootPart = ifElse(b.splitBoot, fmt.Sprintf("/dev/mapper/%sp2", filepath.Base(b.loDevice)), b.bootPart)
|
||||||
if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil {
|
if b.isLuksEnabled() {
|
||||||
|
logrus.Infof("encrypting root partition")
|
||||||
|
f, err := os.CreateTemp("", "key")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
if _, err := f.WriteString(b.luksPassword); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// cryptsetup luksFormat --batch-mode --verify-passphrase --type luks2 $ROOT_DEVICE $KEY_FILE
|
||||||
|
if err := exec.Run(ctx, "cryptsetup", "luksFormat", "--batch-mode", "--type", "luks2", b.rootPart, f.Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.cryptRoot = fmt.Sprintf("d2vm-%s-root", uuid.New().String())
|
||||||
|
// cryptsetup open -d $KEY_FILE $ROOT_DEVICE $ROOT_LABEL
|
||||||
|
if err := exec.Run(ctx, "cryptsetup", "open", "--key-file", f.Name(), b.rootPart, b.cryptRoot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.cryptPart = b.rootPart
|
||||||
|
b.rootPart = "/dev/mapper/root"
|
||||||
|
b.mappedCryptRoot = filepath.Join("/dev/mapper", b.cryptRoot)
|
||||||
|
logrus.Infof("creating raw image file system")
|
||||||
|
if err := exec.Run(ctx, "mkfs.ext4", b.mappedCryptRoot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logrus.Infof("creating raw image file system")
|
||||||
|
if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !b.splitBoot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := exec.Run(ctx, "mount", b.loPart, b.mntPoint); err != nil {
|
if b.bootFS.IsFat() {
|
||||||
|
err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart)
|
||||||
|
} else {
|
||||||
|
err = exec.Run(ctx, "mkfs.ext4", b.bootPart)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -257,99 +353,118 @@ func (b *builder) mountImg(ctx context.Context) error {
|
|||||||
func (b *builder) unmountImg(ctx context.Context) error {
|
func (b *builder) unmountImg(ctx context.Context) error {
|
||||||
logrus.Infof("unmounting raw image")
|
logrus.Infof("unmounting raw image")
|
||||||
var merr error
|
var merr error
|
||||||
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
|
if b.splitBoot {
|
||||||
merr = multierr.Append(merr, err)
|
merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")))
|
||||||
}
|
}
|
||||||
if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil {
|
merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint))
|
||||||
merr = multierr.Append(merr, err)
|
if b.isLuksEnabled() {
|
||||||
|
merr = multierr.Append(merr, exec.Run(ctx, "cryptsetup", "close", b.mappedCryptRoot))
|
||||||
}
|
}
|
||||||
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
|
return multierr.Combine(
|
||||||
merr = multierr.Append(merr, err)
|
merr,
|
||||||
}
|
exec.Run(ctx, "kpartx", "-d", b.loDevice),
|
||||||
return merr
|
exec.Run(ctx, "losetup", "-d", b.loDevice),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) copyRootFS(ctx context.Context) error {
|
func (b *builder) copyRootFS(ctx context.Context) error {
|
||||||
logrus.Infof("copying rootfs to raw image")
|
logrus.Infof("copying rootfs to raw image")
|
||||||
if err := exec.Run(ctx, "tar", "-xvf", b.src, "-C", b.mntPoint); err != nil {
|
if err := b.img.Flatten(ctx, b.mntPoint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) setupRootFS(ctx context.Context) error {
|
func diskUUID(ctx context.Context, disk string) (string, error) {
|
||||||
|
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", disk)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(o, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *builder) setupRootFS(ctx context.Context) (err error) {
|
||||||
logrus.Infof("setting up rootfs")
|
logrus.Infof("setting up rootfs")
|
||||||
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", b.loPart)
|
b.rootUUID, err = diskUUID(ctx, ifElse(b.isLuksEnabled(), b.mappedCryptRoot, b.rootPart))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.diskUUD = strings.TrimSuffix(o, "\n")
|
var fstab string
|
||||||
fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD)
|
if b.splitBoot {
|
||||||
if err := b.chWriteFile("/etc/fstab", fstab, 0644); err != nil {
|
b.bootUUID, err = diskUUID(ctx, b.bootPart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b.isLuksEnabled() {
|
||||||
|
b.cryptUUID, err = diskUUID(ctx, b.cryptPart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot %s errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID, b.bootFS.linux())
|
||||||
|
} else {
|
||||||
|
b.bootUUID = b.rootUUID
|
||||||
|
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID)
|
||||||
|
}
|
||||||
|
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.chWriteFile("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
|
if err := b.chWriteFileIfNotExist("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.chWriteFile("/etc/hostname", "localhost", 0644); err != nil {
|
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := b.chWriteFile("/etc/hosts", hosts, 0644); err != nil {
|
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll("/ur/sbin/policy-rc.d"); err != nil {
|
// TODO(adphi): is it the righ fix ?
|
||||||
|
if err := os.RemoveAll("/usr/sbin/policy-rc.d"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
|
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if b.osRelease.ID != ReleaseAlpine {
|
|
||||||
|
switch b.osRelease.ID {
|
||||||
|
case ReleaseAlpine:
|
||||||
|
by, err := os.ReadFile(b.chPath("/etc/inittab"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
|
||||||
|
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
by, err := os.ReadFile(b.chPath("/etc/inittab"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
|
|
||||||
if err := b.chWriteFile("/etc/inittab", string(by), 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.chWriteFile("/etc/network/interfaces", "", 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) installKernel(ctx context.Context) error {
|
func (b *builder) cmdline(_ context.Context) string {
|
||||||
logrus.Infof("installing linux kernel")
|
if !b.isLuksEnabled() {
|
||||||
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
|
return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
var sysconfig string
|
|
||||||
switch b.osRelease.ID {
|
switch b.osRelease.ID {
|
||||||
case ReleaseUbuntu:
|
|
||||||
sysconfig = syslinuxCfgUbuntu
|
|
||||||
case ReleaseDebian:
|
|
||||||
sysconfig = syslinuxCfgDebian
|
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
sysconfig = syslinuxCfgAlpine
|
return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra)
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS:
|
||||||
sysconfig = syslinuxCfgCentOS
|
return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
|
// for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts...
|
||||||
|
// see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions
|
||||||
|
// and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html
|
||||||
|
return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra)
|
||||||
}
|
}
|
||||||
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) setupMBR(ctx context.Context) error {
|
func (b *builder) installBootloader(ctx context.Context) error {
|
||||||
logrus.Infof("writing MBR")
|
logrus.Infof("installing bootloader")
|
||||||
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", b.mbrPath), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil {
|
return b.bootloader.Setup(ctx, b.loDevice, b.mntPoint, b.cmdline(ctx))
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) convert2Img(ctx context.Context) error {
|
func (b *builder) convert2Img(ctx context.Context) error {
|
||||||
@ -361,22 +476,41 @@ func (b *builder) chWriteFile(path string, content string, perm os.FileMode) err
|
|||||||
return os.WriteFile(b.chPath(path), []byte(content), perm)
|
return os.WriteFile(b.chPath(path), []byte(content), perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *builder) chWriteFileIfNotExist(path string, content string, perm os.FileMode) error {
|
||||||
|
if i, err := os.Stat(b.chPath(path)); err == nil && i.Size() != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.WriteFile(b.chPath(path), []byte(content), perm)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *builder) chPath(path string) string {
|
func (b *builder) chPath(path string) string {
|
||||||
return fmt.Sprintf("%s%s", b.mntPoint, path)
|
return fmt.Sprintf("%s%s", b.mntPoint, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func block(path string, size int64) error {
|
func (b *builder) isLuksEnabled() bool {
|
||||||
|
return b.luksPassword != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *builder) Close() error {
|
||||||
|
return b.img.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func block(path string, size uint64) error {
|
||||||
f, err := os.Create(path)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
return f.Truncate(size)
|
return f.Truncate(int64(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkDependencies() error {
|
func (b *builder) checkDependencies() error {
|
||||||
var merr error
|
var merr error
|
||||||
for _, v := range []string{"mount", "blkid", "tar", "kpartx", "losetup", "qemu-img", "extlinux", "dd", "mkfs", "fdisk"} {
|
deps := []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "dd", "mkfs.ext4", "cryptsetup"}
|
||||||
|
if _, ok := b.bootloader.(*syslinux); ok {
|
||||||
|
deps = append(deps, "extlinux")
|
||||||
|
}
|
||||||
|
for _, v := range deps {
|
||||||
if _, err := exec2.LookPath(v); err != nil {
|
if _, err := exec2.LookPath(v); err != nil {
|
||||||
merr = multierr.Append(merr, err)
|
merr = multierr.Append(merr, err)
|
||||||
}
|
}
|
||||||
@ -387,3 +521,10 @@ func checkDependencies() error {
|
|||||||
func OutputFormats() []string {
|
func OutputFormats() []string {
|
||||||
return formats[:]
|
return formats[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ifElse(v bool, t string, f string) string {
|
||||||
|
if v {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
@ -15,6 +15,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -23,34 +27,98 @@ import (
|
|||||||
|
|
||||||
"go.linka.cloud/d2vm"
|
"go.linka.cloud/d2vm"
|
||||||
"go.linka.cloud/d2vm/pkg/docker"
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
"go.linka.cloud/d2vm/pkg/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file = "Dockerfile"
|
file = "Dockerfile"
|
||||||
tag = uuid.New().String()
|
tag = "d2vm-" + uuid.New().String()
|
||||||
buildArgs []string
|
buildArgs []string
|
||||||
buildCmd = &cobra.Command{
|
buildCmd = &cobra.Command{
|
||||||
Use: "build [context directory]",
|
Use: "build [context directory]",
|
||||||
Short: "Build a vm image from Dockerfile",
|
Short: "Build a vm image from Dockerfile",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// TODO(adphi): resolve context path
|
||||||
|
if runtime.GOOS != "linux" || !isRoot() {
|
||||||
|
ctxAbsPath, err := filepath.Abs(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if file == "" {
|
||||||
|
file = filepath.Join(args[0], "Dockerfile")
|
||||||
|
}
|
||||||
|
dockerFileAbsPath, err := filepath.Abs(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(dockerFileAbsPath, ctxAbsPath) {
|
||||||
|
return fmt.Errorf("Dockerfile must be in the context directory path")
|
||||||
|
}
|
||||||
|
outputPath, err := filepath.Abs(output)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
in = ctxAbsPath
|
||||||
|
out = filepath.Dir(outputPath)
|
||||||
|
)
|
||||||
|
dargs := os.Args[2:]
|
||||||
|
for i, v := range dargs {
|
||||||
|
switch v {
|
||||||
|
case file:
|
||||||
|
rel, err := filepath.Rel(in, dockerFileAbsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to construct Dockerfile container paths: %w", err)
|
||||||
|
}
|
||||||
|
dargs[i] = filepath.Join("/in", rel)
|
||||||
|
case output:
|
||||||
|
dargs[i] = filepath.Join("/out", filepath.Base(output))
|
||||||
|
case args[0]:
|
||||||
|
dargs[i] = "/in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...)
|
||||||
|
}
|
||||||
|
if err := validateFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if debug {
|
if file == "" {
|
||||||
exec.Run = exec.RunStdout
|
file = filepath.Join(args[0], "Dockerfile")
|
||||||
}
|
}
|
||||||
logrus.Infof("building docker image from %s", file)
|
logrus.Infof("building docker image from %s", file)
|
||||||
dargs := []string{"build", "-t", tag, "-f", file, args[0]}
|
if err := docker.Build(cmd.Context(), pull, tag, file, args[0], platform, buildArgs...); err != nil {
|
||||||
for _, v := range buildArgs {
|
|
||||||
dargs = append(dargs, "--build-arg", v)
|
|
||||||
}
|
|
||||||
if err := docker.Cmd(cmd.Context(), dargs...); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
|
if err := d2vm.Convert(
|
||||||
|
cmd.Context(),
|
||||||
|
tag,
|
||||||
|
d2vm.WithSize(size),
|
||||||
|
d2vm.WithPassword(password),
|
||||||
|
d2vm.WithOutput(output),
|
||||||
|
d2vm.WithCmdLineExtra(cmdLineExtra),
|
||||||
|
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
||||||
|
d2vm.WithBootLoader(bootloader),
|
||||||
|
d2vm.WithRaw(raw),
|
||||||
|
d2vm.WithSplitBoot(splitBoot),
|
||||||
|
d2vm.WithBootSize(bootSize),
|
||||||
|
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
|
||||||
|
d2vm.WithLuksPassword(luksPassword),
|
||||||
|
d2vm.WithKeepCache(keepCache),
|
||||||
|
d2vm.WithPlatform(platform),
|
||||||
|
d2vm.WithPull(false),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if uid, ok := sudoUser(); ok {
|
||||||
|
if err := os.Chown(output, uid, uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maybeMakeContainerDisk(cmd.Context())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -58,13 +126,8 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(buildCmd)
|
rootCmd.AddCommand(buildCmd)
|
||||||
|
|
||||||
buildCmd.Flags().StringVarP(&file, "file", "f", "Dockerfile", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
|
buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile")
|
||||||
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
|
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
|
||||||
|
|
||||||
buildCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
buildCmd.Flags().AddFlagSet(buildFlags())
|
||||||
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
|
||||||
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
|
|
||||||
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
|
||||||
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
|
||||||
buildCmd.Flags().BoolVar(&force, "force", false, "Override output image")
|
|
||||||
}
|
}
|
||||||
|
43
cmd/d2vm/container_disk.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm"
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func maybeMakeContainerDisk(ctx context.Context) error {
|
||||||
|
if containerDiskTag == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logrus.Infof("creating container disk image %s", containerDiskTag)
|
||||||
|
if err := d2vm.MakeContainerDisk(ctx, output, containerDiskTag, platform); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !push {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logrus.Infof("pushing container disk image %s", containerDiskTag)
|
||||||
|
if err := docker.Push(ctx, containerDiskTag); err != nil {
|
||||||
|
return fmt.Errorf("failed to push container disk: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -25,77 +25,95 @@ import (
|
|||||||
|
|
||||||
"go.linka.cloud/d2vm"
|
"go.linka.cloud/d2vm"
|
||||||
"go.linka.cloud/d2vm/pkg/docker"
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
"go.linka.cloud/d2vm/pkg/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pull = false
|
|
||||||
|
|
||||||
convertCmd = &cobra.Command{
|
convertCmd = &cobra.Command{
|
||||||
Use: "convert [docker image]",
|
Use: "convert [docker image]",
|
||||||
Short: "Convert Docker image to vm image",
|
Short: "Convert Docker image to vm image",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
img := args[0]
|
if runtime.GOOS != "linux" || !isRoot() {
|
||||||
tag := "latest"
|
abs, err := filepath.Abs(output)
|
||||||
if parts := strings.Split(img, ":"); len(parts) > 1 {
|
if err != nil {
|
||||||
img, tag = parts[0], parts[1]
|
return err
|
||||||
|
}
|
||||||
|
out := filepath.Dir(abs)
|
||||||
|
dargs := os.Args[2:]
|
||||||
|
for i, v := range dargs {
|
||||||
|
if v == output {
|
||||||
|
dargs[i] = filepath.Join("/out", filepath.Base(output))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...)
|
||||||
|
}
|
||||||
|
if err := validateFlags(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
img := args[0]
|
||||||
if !force {
|
|
||||||
return fmt.Errorf("%s already exists", output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if debug {
|
|
||||||
exec.Run = exec.RunStdout
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
|
||||||
if !force {
|
|
||||||
return fmt.Errorf("%s already exists", output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
found := false
|
found := false
|
||||||
if !pull {
|
if !pull {
|
||||||
o, _, err := docker.CmdOut(cmd.Context(), "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", img)
|
imgs, err := docker.ImageList(cmd.Context(), img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
found = strings.TrimSuffix(o, "\n") == fmt.Sprintf("%s:%s", img, tag)
|
found = len(imgs) == 1 && imgs[0] == img
|
||||||
if found {
|
if found {
|
||||||
logrus.Infof("using local image %s:%s", img, tag)
|
logrus.Infof("using local image %s", img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pull || !found {
|
if pull || !found {
|
||||||
logrus.Infof("pulling image %s", img)
|
logrus.Infof("pulling image %s", img)
|
||||||
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
|
if err := docker.Pull(cmd.Context(), platform, img); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d2vm.Convert(cmd.Context(), img, size, password, output, format)
|
if err := d2vm.Convert(
|
||||||
|
cmd.Context(),
|
||||||
|
img,
|
||||||
|
d2vm.WithSize(size),
|
||||||
|
d2vm.WithPassword(password),
|
||||||
|
d2vm.WithOutput(output),
|
||||||
|
d2vm.WithCmdLineExtra(cmdLineExtra),
|
||||||
|
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
||||||
|
d2vm.WithBootLoader(bootloader),
|
||||||
|
d2vm.WithRaw(raw),
|
||||||
|
d2vm.WithSplitBoot(splitBoot),
|
||||||
|
d2vm.WithBootSize(bootSize),
|
||||||
|
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
|
||||||
|
d2vm.WithLuksPassword(luksPassword),
|
||||||
|
d2vm.WithKeepCache(keepCache),
|
||||||
|
d2vm.WithPlatform(platform),
|
||||||
|
d2vm.WithPull(pull),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// set user permissions on the output file if the command was run with sudo
|
||||||
|
if uid, ok := sudoUser(); ok {
|
||||||
|
if err := os.Chown(output, uid, uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maybeMakeContainerDisk(cmd.Context())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseSize(s string) (int64, error) {
|
func parseSize(s string) (uint64, error) {
|
||||||
var v datasize.ByteSize
|
var v datasize.ByteSize
|
||||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
if err := v.UnmarshalText([]byte(s)); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return int64(v), nil
|
return uint64(v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
|
convertCmd.Flags().AddFlagSet(buildFlags())
|
||||||
convertCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
|
||||||
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
|
||||||
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
|
|
||||||
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
|
||||||
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
|
||||||
convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image")
|
|
||||||
rootCmd.AddCommand(convertCmd)
|
rootCmd.AddCommand(convertCmd)
|
||||||
}
|
}
|
||||||
|
43
cmd/d2vm/docs.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/cobra/doc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var docsCmd = &cobra.Command{
|
||||||
|
Use: "docs",
|
||||||
|
Short: "Generate documentation",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := os.MkdirAll(args[0], 0755); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd.Root().DisableAutoGenTag = true
|
||||||
|
if err := doc.GenMarkdownTree(cmd.Root(), args[0]); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(docsCmd)
|
||||||
|
}
|
120
cmd/d2vm/flags.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
output = "disk0.qcow2"
|
||||||
|
size = "1G"
|
||||||
|
password = ""
|
||||||
|
force = false
|
||||||
|
raw bool
|
||||||
|
pull = false
|
||||||
|
cmdLineExtra = ""
|
||||||
|
containerDiskTag = ""
|
||||||
|
push bool
|
||||||
|
networkManager string
|
||||||
|
bootloader string
|
||||||
|
splitBoot bool
|
||||||
|
bootSize uint64
|
||||||
|
bootFS string
|
||||||
|
luksPassword string
|
||||||
|
|
||||||
|
keepCache bool
|
||||||
|
platform string
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateFlags() error {
|
||||||
|
switch platform {
|
||||||
|
case "linux/amd64":
|
||||||
|
if bootloader == "" {
|
||||||
|
bootloader = "syslinux"
|
||||||
|
}
|
||||||
|
case "linux/arm64", "linux/aarch64":
|
||||||
|
platform = "linux/arm64"
|
||||||
|
if bootloader == "" {
|
||||||
|
bootloader = "grub-efi"
|
||||||
|
}
|
||||||
|
if bootloader != "grub-efi" {
|
||||||
|
return fmt.Errorf("unsupported bootloader for platform %s: %s, only grub-efi is supported", platform, bootloader)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
|
||||||
|
}
|
||||||
|
if luksPassword != "" && !splitBoot {
|
||||||
|
logrus.Warnf("luks password is set: enabling split boot")
|
||||||
|
splitBoot = true
|
||||||
|
}
|
||||||
|
if bootFS := d2vm.BootFS(bootFS); bootFS != "" && !bootFS.IsSupported() {
|
||||||
|
return fmt.Errorf("invalid boot filesystem: %s", bootFS)
|
||||||
|
}
|
||||||
|
if bootFS != "" && !splitBoot {
|
||||||
|
logrus.Warnf("boot filesystem is set: enabling split boot")
|
||||||
|
splitBoot = true
|
||||||
|
}
|
||||||
|
efi := bootloader == "grub-efi" || bootloader == "grub"
|
||||||
|
if efi && !splitBoot {
|
||||||
|
logrus.Warnf("grub-efi bootloader is set: enabling split boot")
|
||||||
|
splitBoot = true
|
||||||
|
}
|
||||||
|
if efi && bootFS != "" && bootFS != "fat32" {
|
||||||
|
return fmt.Errorf("grub-efi bootloader only supports fat32 boot filesystem")
|
||||||
|
}
|
||||||
|
if efi && bootFS == "" {
|
||||||
|
logrus.Warnf("grub-efi bootloader is set: enabling fat32 boot filesystem")
|
||||||
|
bootFS = "fat32"
|
||||||
|
}
|
||||||
|
if push && tag == "" {
|
||||||
|
return fmt.Errorf("tag is required when pushing container disk image")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
||||||
|
if !force {
|
||||||
|
return fmt.Errorf("%s already exists", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFlags() *pflag.FlagSet {
|
||||||
|
flags := pflag.NewFlagSet("build", pflag.ExitOnError)
|
||||||
|
flags.StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
|
flags.StringVarP(&password, "password", "p", "", "Optional root user password")
|
||||||
|
flags.StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
|
flags.BoolVar(&force, "force", false, "Override output qcow2 image")
|
||||||
|
flags.StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
|
||||||
|
flags.StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
|
||||||
|
flags.BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
|
||||||
|
flags.StringVarP(&containerDiskTag, "tag", "t", "", "Container disk Docker image tag")
|
||||||
|
flags.BoolVar(&push, "push", false, "Push the container disk image to the registry")
|
||||||
|
flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition")
|
||||||
|
flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB")
|
||||||
|
flags.StringVar(&bootFS, "boot-fs", "", "Filesystem to use for the boot partition, ext4 or fat32")
|
||||||
|
flags.StringVar(&bootloader, "bootloader", "", "Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64")
|
||||||
|
flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted")
|
||||||
|
flags.BoolVar(&keepCache, "keep-cache", false, "Keep the images after the build")
|
||||||
|
flags.StringVar(&platform, "platform", d2vm.Arch, "Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported")
|
||||||
|
flags.BoolVar(&pull, "pull", false, "Always pull docker image")
|
||||||
|
return flags
|
||||||
|
}
|
126
cmd/d2vm/main.go
@ -15,25 +15,52 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm"
|
"go.linka.cloud/d2vm"
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
output = "disk0.qcow2"
|
verbose = false
|
||||||
size = "1G"
|
timeFormat = ""
|
||||||
password = "root"
|
format = "qcow2"
|
||||||
force = false
|
|
||||||
debug = false
|
|
||||||
format = "qcow2"
|
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "d2vm",
|
Use: "d2vm",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
Version: d2vm.Version,
|
Version: d2vm.Version,
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
switch timeFormat {
|
||||||
|
case "full", "f":
|
||||||
|
case "relative", "rel", "r":
|
||||||
|
case "none", "":
|
||||||
|
default:
|
||||||
|
logrus.Fatalf("invalid time format: %s. Valid format: 'relative', 'full'", timeFormat)
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
logrus.SetLevel(logrus.TraceLevel)
|
||||||
|
}
|
||||||
|
exec.SetDebug(verbose)
|
||||||
|
|
||||||
|
// make the zsh completion work when sourced with `source <(d2vm completion zsh)`
|
||||||
|
if cmd.Name() == "zsh" && cmd.Parent() != nil && cmd.Parent().Name() == "completion" {
|
||||||
|
zshHead := fmt.Sprintf("#compdef %[1]s\ncompdef _%[1]s %[1]s\n", cmd.Root().Name())
|
||||||
|
cmd.OutOrStdout().Write([]byte(zshHead))
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,5 +68,90 @@ func main() {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rootCmd.ExecuteContext(ctx)
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, os.Interrupt, os.Kill)
|
||||||
|
go func() {
|
||||||
|
<-sigs
|
||||||
|
fmt.Println()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "debug", "d", false, "Enable Debug output")
|
||||||
|
rootCmd.PersistentFlags().MarkDeprecated("debug", "use -v instead")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable Verbose output")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&timeFormat, "time", "none", "Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)'")
|
||||||
|
color.NoColor = false
|
||||||
|
logrus.StandardLogger().Formatter = &logfmtFormatter{start: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
red = 31
|
||||||
|
yellow = 33
|
||||||
|
blue = 36
|
||||||
|
white = 39
|
||||||
|
gray = 90
|
||||||
|
)
|
||||||
|
|
||||||
|
type logfmtFormatter struct {
|
||||||
|
start time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *logfmtFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
var c *color.Color
|
||||||
|
switch entry.Level {
|
||||||
|
case logrus.DebugLevel, logrus.TraceLevel:
|
||||||
|
c = color.New(gray)
|
||||||
|
case logrus.WarnLevel:
|
||||||
|
c = color.New(yellow)
|
||||||
|
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
|
||||||
|
c = color.New(red)
|
||||||
|
default:
|
||||||
|
c = color.New(white)
|
||||||
|
}
|
||||||
|
msg := entry.Message
|
||||||
|
if len(entry.Message) > 0 && entry.Level < logrus.DebugLevel {
|
||||||
|
msg = strings.ToTitle(string(msg[0])) + msg[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch timeFormat {
|
||||||
|
case "full", "f":
|
||||||
|
_, err = c.Fprintf(&b, "[%s] %s\n", entry.Time.Format("2006-01-02 15:04:05"), entry.Message)
|
||||||
|
case "relative", "rel", "r":
|
||||||
|
_, err = c.Fprintf(&b, "[%5v] %s\n", entry.Time.Sub(f.start).Truncate(time.Second).String(), msg)
|
||||||
|
default:
|
||||||
|
_, err = c.Fprintln(&b, msg)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRoot() bool {
|
||||||
|
return os.Geteuid() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func sudoUser() (uid int, sudo bool) {
|
||||||
|
// if we are not running on linux, docker handle files user's permissions,
|
||||||
|
// so we don't need to check for sudo here
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v := os.Getenv("SUDO_UID")
|
||||||
|
if v == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
uid, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("invalid SUDO_UID: %s", v)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return uid, true
|
||||||
}
|
}
|
||||||
|
36
cmd/d2vm/run.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/cmd/d2vm/run"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Run the virtual machine image",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
|
||||||
|
runCmd.AddCommand(run.VboxCmd)
|
||||||
|
runCmd.AddCommand(run.QemuCmd)
|
||||||
|
runCmd.AddCommand(run.HetznerCmd)
|
||||||
|
}
|
1
cmd/d2vm/run/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Shamelessly taken from [linuxkit](https://github.com/linuxkit/linuxkit/tree/master/src/cmd/linuxkit)
|
387
cmd/d2vm/run/hetzner.go
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/svenwiltink/sparsecat"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu_img"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hetznerTokenEnv = "HETZNER_TOKEN"
|
||||||
|
serverImg = "ubuntu-20.04"
|
||||||
|
vmBlock = "sda"
|
||||||
|
vmBlockPath = "/dev/" + vmBlock
|
||||||
|
sparsecatPath = "/usr/local/bin/sparsecat"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hetznerVMType = "cx11"
|
||||||
|
hetznerToken = ""
|
||||||
|
// ash-dc1 fsn1-dc14 hel1-dc2 nbg1-dc3
|
||||||
|
hetznerDatacenter = "hel1-dc2"
|
||||||
|
hetznerServerName = "d2vm"
|
||||||
|
hetznerSSHUser = ""
|
||||||
|
hetznerSSHKeyPath = ""
|
||||||
|
hetznerRemove = false
|
||||||
|
|
||||||
|
HetznerCmd = &cobra.Command{
|
||||||
|
Use: "hetzner [options] image-path",
|
||||||
|
Short: "Run the virtual machine image on Hetzner Cloud",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: Hetzner,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
HetznerCmd.Flags().StringVar(&hetznerToken, "token", "", "Hetzner Cloud API token [$"+hetznerTokenEnv+"]")
|
||||||
|
HetznerCmd.Flags().StringVarP(&hetznerSSHUser, "user", "u", "root", "d2vm image ssh user")
|
||||||
|
HetznerCmd.Flags().StringVarP(&hetznerSSHKeyPath, "ssh-key", "i", "", "d2vm image identity key")
|
||||||
|
HetznerCmd.Flags().BoolVar(&hetznerRemove, "rm", false, "remove server when done")
|
||||||
|
HetznerCmd.Flags().StringVarP(&hetznerServerName, "name", "n", "d2vm", "d2vm server name")
|
||||||
|
HetznerCmd.Flags().StringVarP(&hetznerVMType, "type", "t", hetznerVMType, "d2vm server type")
|
||||||
|
HetznerCmd.Flags().StringVarP(&hetznerDatacenter, "location", "l", hetznerDatacenter, "d2vm server location")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hetzner(cmd *cobra.Command, args []string) {
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
if err := runHetzner(ctx, args[0], cmd.InOrStdin(), cmd.ErrOrStderr(), cmd.OutOrStdout()); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.Writer, stdout io.Writer) error {
|
||||||
|
i, err := qemu_img.Info(ctx, imgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i.Format != "raw" {
|
||||||
|
logrus.Warnf("image format is %s, expected raw", i.Format)
|
||||||
|
rawPath := filepath.Join(os.TempDir(), "d2vm", "run", filepath.Base(imgPath)+".raw")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(rawPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(rawPath)
|
||||||
|
logrus.Infof("converting image to raw: %s", rawPath)
|
||||||
|
if err := qemu_img.Convert(ctx, "raw", imgPath, rawPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imgPath = rawPath
|
||||||
|
i, err = qemu_img.Info(ctx, imgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
src, err := os.Open(imgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
c := hcloud.NewClient(hcloud.WithToken(GetStringValue(hetznerTokenEnv, hetznerToken, "")))
|
||||||
|
st, _, err := c.ServerType.GetByName(ctx, hetznerVMType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
arch := "amd64"
|
||||||
|
harch := hcloud.ArchitectureX86
|
||||||
|
if strings.HasPrefix(strings.ToLower(hetznerVMType), "cax") {
|
||||||
|
harch = hcloud.ArchitectureARM
|
||||||
|
arch = "arm64"
|
||||||
|
}
|
||||||
|
sparsecatBin, err := Sparsecat(arch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imgs, _, err := c.Image.List(ctx, hcloud.ImageListOpts{Name: serverImg, Architecture: []hcloud.Architecture{harch}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(imgs) == 0 {
|
||||||
|
return fmt.Errorf("no image found with name %s", serverImg)
|
||||||
|
}
|
||||||
|
l, _, err := c.Location.Get(ctx, hetznerDatacenter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("creating server %s", hetznerServerName)
|
||||||
|
sres, _, err := c.Server.Create(ctx, hcloud.ServerCreateOpts{
|
||||||
|
Name: hetznerServerName,
|
||||||
|
ServerType: st,
|
||||||
|
Image: imgs[0],
|
||||||
|
Location: l,
|
||||||
|
StartAfterCreate: hcloud.Ptr(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
remove := true
|
||||||
|
defer func() {
|
||||||
|
if !remove && !hetznerRemove {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logrus.Infof("removing server %s", hetznerServerName)
|
||||||
|
// we use context.Background() here because we don't want the request to fail if the context has been cancelled
|
||||||
|
if _, err := c.Server.Delete(context.Background(), sres.Server); err != nil {
|
||||||
|
logrus.Fatalf("failed to remove server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, errs := c.Action.WatchProgress(ctx, sres.Action)
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("server created with ip: %s", sres.Server.PublicNet.IPv4.IP.String())
|
||||||
|
logrus.Infof("enabling server rescue mode")
|
||||||
|
rres, _, err := c.Server.EnableRescue(ctx, sres.Server, hcloud.ServerEnableRescueOpts{Type: hcloud.ServerRescueTypeLinux64})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, errs = c.Action.WatchProgress(ctx, rres.Action)
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("powering on server")
|
||||||
|
pres, _, err := c.Server.Poweron(ctx, sres.Server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, errs = c.Action.WatchProgress(ctx, pres)
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("connecting to server via ssh")
|
||||||
|
sc, err := dialSSHWithTimeout(sres.Server.PublicNet.IPv4.IP.String(), "root", rres.RootPassword, time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sc.Close()
|
||||||
|
logrus.Infof("connection established")
|
||||||
|
sftpc, err := sftp.NewClient(sc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := sftpc.Create(sparsecatPath)
|
||||||
|
logrus.Debugf("creating sparsecat on remote host")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := sftpc.Chmod(sparsecatPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.Copy(f, bytes.NewReader(sparsecatBin)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serrs := make(chan error, 2)
|
||||||
|
go func() {
|
||||||
|
serrs <- func() error {
|
||||||
|
s, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
logrus.Infof("installing cloud-guest-utils on rescue server")
|
||||||
|
cmd := "apt update && apt install -y cloud-guest-utils"
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
if b, err := s.CombinedOutput(cmd); err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
serrs <- func() error {
|
||||||
|
wses, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer wses.Close()
|
||||||
|
logrus.Infof("writing image to %s", vmBlockPath)
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
var r io.Reader
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
r = sparsecat.NewEncoder(src)
|
||||||
|
} else {
|
||||||
|
r = src
|
||||||
|
}
|
||||||
|
pr := newProgressReader(r)
|
||||||
|
wses.Stdin = pr
|
||||||
|
go func() {
|
||||||
|
tk := time.NewTicker(time.Second)
|
||||||
|
last := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tk.C:
|
||||||
|
b := pr.Progress()
|
||||||
|
logrus.Infof("%s / %d%% transfered (%s/s)", humanize.Bytes(uint64(b)), int(float64(b)/float64(i.VirtualSize)*100), humanize.Bytes(uint64(b-last)))
|
||||||
|
last = b
|
||||||
|
case <-ctx.Done():
|
||||||
|
logrus.Warnf("context cancelled")
|
||||||
|
return
|
||||||
|
case <-done:
|
||||||
|
logrus.Infof("transfer finished")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var cmd string
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cmd = fmt.Sprintf("%s -r -disable-sparse-target -of %s", sparsecatPath, vmBlockPath)
|
||||||
|
} else {
|
||||||
|
cmd = fmt.Sprintf("dd of=%s", vmBlockPath)
|
||||||
|
}
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
if b, err := wses.CombinedOutput(cmd); err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
} else {
|
||||||
|
logrus.Debugf(string(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
select {
|
||||||
|
case err := <-serrs:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nses, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer nses.Close()
|
||||||
|
// retrieve the partition number
|
||||||
|
cmd := fmt.Sprintf("ls %s*", vmBlockPath)
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
b, err := nses.CombinedOutput(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
}
|
||||||
|
logrus.Debugf(string(b))
|
||||||
|
parts := strings.Fields(strings.TrimSuffix(string(b), "\n"))
|
||||||
|
vmPartNumber, err := strconv.Atoi(strings.Replace(parts[len(parts)-1], vmBlockPath, "", 1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gses, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gses.Close()
|
||||||
|
logrus.Infof("resizing disk partition")
|
||||||
|
cmd = fmt.Sprintf("growpart %s %d", vmBlockPath, vmPartNumber)
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
if b, err := gses.CombinedOutput(cmd); err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
} else {
|
||||||
|
logrus.Debugf(string(b))
|
||||||
|
}
|
||||||
|
cses, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cses.Close()
|
||||||
|
logrus.Infof("checking disk partition")
|
||||||
|
cmd = fmt.Sprintf("e2fsck -yf %s%d", vmBlockPath, vmPartNumber)
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
if b, err := cses.CombinedOutput(cmd); err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
} else {
|
||||||
|
logrus.Debugf(string(b))
|
||||||
|
}
|
||||||
|
eses, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer eses.Close()
|
||||||
|
logrus.Infof("extending partition file system")
|
||||||
|
cmd = fmt.Sprintf("resize2fs %s%d", vmBlockPath, vmPartNumber)
|
||||||
|
logrus.Debugf("$ %s", cmd)
|
||||||
|
if b, err := eses.CombinedOutput(cmd); err != nil {
|
||||||
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
}
|
||||||
|
logrus.Infof("rebooting server")
|
||||||
|
rbres, _, err := c.Server.Reboot(ctx, sres.Server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, errs = c.Action.WatchProgress(ctx, rbres)
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
remove = false
|
||||||
|
logrus.Infof("waiting for server to be ready")
|
||||||
|
t := time.NewTimer(time.Minute)
|
||||||
|
wait:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return fmt.Errorf("ssh connection timeout")
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:22", sres.Server.PublicNet.IPv4.IP.String()))
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
break wait
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrus.Infof("server ready")
|
||||||
|
args := []string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}
|
||||||
|
if hetznerSSHKeyPath != "" {
|
||||||
|
args = append(args, "-i", hetznerSSHKeyPath)
|
||||||
|
}
|
||||||
|
args = append(args, fmt.Sprintf("%s@%s", hetznerSSHUser, sres.Server.PublicNet.IPv4.IP.String()))
|
||||||
|
logrus.Debugf("$ ssh %s", strings.Join(args, " "))
|
||||||
|
sshCmd := exec.CommandContext(ctx, "ssh", args...)
|
||||||
|
sshCmd.Stdin = stdin
|
||||||
|
sshCmd.Stderr = stderr
|
||||||
|
sshCmd.Stdout = stdout
|
||||||
|
if err := sshCmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
127
cmd/d2vm/run/qemu.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
qemuNetworkingNone string = "none"
|
||||||
|
qemuNetworkingUser = "user"
|
||||||
|
qemuNetworkingTap = "tap"
|
||||||
|
qemuNetworkingBridge = "bridge"
|
||||||
|
qemuNetworkingDefault = qemuNetworkingUser
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultArch string
|
||||||
|
defaultAccel string
|
||||||
|
enableGUI bool
|
||||||
|
disks Disks
|
||||||
|
data string
|
||||||
|
accel string
|
||||||
|
arch string
|
||||||
|
cpus uint
|
||||||
|
mem uint
|
||||||
|
bios string
|
||||||
|
qemuCmd string
|
||||||
|
qemuDetached bool
|
||||||
|
networking string
|
||||||
|
publishFlags MultipleFlag
|
||||||
|
|
||||||
|
QemuCmd = &cobra.Command{
|
||||||
|
Use: "qemu [options] [image-path]",
|
||||||
|
Short: "Run the virtual machine image with qemu",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: Qemu,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "arm64":
|
||||||
|
defaultArch = "aarch64"
|
||||||
|
case "amd64":
|
||||||
|
defaultArch = "x86_64"
|
||||||
|
case "s390x":
|
||||||
|
defaultArch = "s390x"
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case runtime.GOARCH == "s390x":
|
||||||
|
defaultAccel = "kvm"
|
||||||
|
case haveKVM():
|
||||||
|
defaultAccel = "kvm:tcg"
|
||||||
|
case runtime.GOOS == "darwin":
|
||||||
|
defaultAccel = "hvf:tcg"
|
||||||
|
}
|
||||||
|
flags := QemuCmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVar(&enableGUI, "gui", false, "Set qemu to use video output instead of stdio")
|
||||||
|
|
||||||
|
// Paths and settings for disks
|
||||||
|
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
|
||||||
|
|
||||||
|
// VM configuration
|
||||||
|
flags.StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
|
||||||
|
flags.StringVar(&arch, "arch", defaultArch, "Type of architecture to use, e.g. x86_64, aarch64, s390x")
|
||||||
|
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
|
||||||
|
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
|
||||||
|
|
||||||
|
flags.StringVar(&bios, "bios", "", "Path to the optional bios binary")
|
||||||
|
|
||||||
|
// Backend configuration
|
||||||
|
flags.StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
|
||||||
|
flags.BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background")
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
flags.StringVar(&networking, "networking", qemuNetworkingDefault, "Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.`")
|
||||||
|
|
||||||
|
flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Qemu(cmd *cobra.Command, args []string) {
|
||||||
|
path := args[0]
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
var publishedPorts []PublishedPort
|
||||||
|
for _, publish := range publishFlags {
|
||||||
|
p, err := NewPublishedPort(publish)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
publishedPorts = append(publishedPorts, p)
|
||||||
|
}
|
||||||
|
opts := []qemu.Option{
|
||||||
|
qemu.WithDisks(disks...),
|
||||||
|
qemu.WithAccel(accel),
|
||||||
|
qemu.WithArch(arch),
|
||||||
|
qemu.WithCPUs(cpus),
|
||||||
|
qemu.WithMemory(mem),
|
||||||
|
qemu.WithNetworking(networking),
|
||||||
|
qemu.WithStdin(os.Stdin),
|
||||||
|
qemu.WithStdout(os.Stdout),
|
||||||
|
qemu.WithStderr(os.Stderr),
|
||||||
|
qemu.WithBios(bios),
|
||||||
|
}
|
||||||
|
if enableGUI {
|
||||||
|
opts = append(opts, qemu.WithGUI())
|
||||||
|
}
|
||||||
|
if qemuDetached {
|
||||||
|
opts = append(opts, qemu.WithDetached())
|
||||||
|
}
|
||||||
|
if err := qemu.Run(cmd.Context(), path, opts...); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveKVM() bool {
|
||||||
|
_, err := os.Stat("/dev/kvm")
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
349
cmd/d2vm/run/util.go
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
//go:generate env GOOS=linux GOARCH=amd64 go build -o sparsecat-linux-amd64 github.com/svenwiltink/sparsecat/cmd/sparsecat
|
||||||
|
//go:generate env GOOS=linux GOARCH=arm64 go build -o sparsecat-linux-arm64 github.com/svenwiltink/sparsecat/cmd/sparsecat
|
||||||
|
|
||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed sparsecat-linux-amd64
|
||||||
|
var sparsecatAmdBinary []byte
|
||||||
|
|
||||||
|
//go:embed sparsecat-linux-arm64
|
||||||
|
var sparsecatArmBinary []byte
|
||||||
|
|
||||||
|
func Sparsecat(arch string) ([]byte, error) {
|
||||||
|
switch arch {
|
||||||
|
case "amd64":
|
||||||
|
return sparsecatAmdBinary, nil
|
||||||
|
case "arm64":
|
||||||
|
return sparsecatArmBinary, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported architecture: %s", arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle flags with multiple occurrences
|
||||||
|
type MultipleFlag []string
|
||||||
|
|
||||||
|
func (f *MultipleFlag) String() string {
|
||||||
|
return "A multiple flag is a type of flag that can be repeated any number of times"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MultipleFlag) Set(value string) error {
|
||||||
|
*f = append(*f, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MultipleFlag) Type() string {
|
||||||
|
return "multiple-flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStringValue(envKey string, flagVal string, defaultVal string) string {
|
||||||
|
var res string
|
||||||
|
|
||||||
|
// If defined, take the env variable
|
||||||
|
if _, ok := os.LookupEnv(envKey); ok {
|
||||||
|
res = os.Getenv(envKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a flag is specified, this value takes precedence
|
||||||
|
// Ignore cases where the flag carries the default value
|
||||||
|
if flagVal != "" && flagVal != defaultVal {
|
||||||
|
res = flagVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we still don't have a value, use the default
|
||||||
|
if res == "" {
|
||||||
|
res = defaultVal
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIntValue(envKey string, flagVal int, defaultVal int) int {
|
||||||
|
var res int
|
||||||
|
|
||||||
|
// If defined, take the env variable
|
||||||
|
if _, ok := os.LookupEnv(envKey); ok {
|
||||||
|
var err error
|
||||||
|
res, err = strconv.Atoi(os.Getenv(envKey))
|
||||||
|
if err != nil {
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a flag is specified, this value takes precedence
|
||||||
|
// Ignore cases where the flag carries the default value
|
||||||
|
if flagVal > 0 {
|
||||||
|
res = flagVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we still don't have a value, use the default
|
||||||
|
if res == 0 {
|
||||||
|
res = defaultVal
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBoolValue(envKey string, flagVal bool) bool {
|
||||||
|
var res bool
|
||||||
|
|
||||||
|
// If defined, take the env variable
|
||||||
|
if _, ok := os.LookupEnv(envKey); ok {
|
||||||
|
switch os.Getenv(envKey) {
|
||||||
|
case "":
|
||||||
|
res = false
|
||||||
|
case "0":
|
||||||
|
res = false
|
||||||
|
case "false":
|
||||||
|
res = false
|
||||||
|
case "FALSE":
|
||||||
|
res = false
|
||||||
|
case "1":
|
||||||
|
res = true
|
||||||
|
default:
|
||||||
|
// catches "true", "TRUE" or anything else
|
||||||
|
res = true
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a flag is specified, this value takes precedence
|
||||||
|
if res != flagVal {
|
||||||
|
res = flagVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToIntArray(l string, sep string) ([]int, error) {
|
||||||
|
var err error
|
||||||
|
if l == "" {
|
||||||
|
return []int{}, err
|
||||||
|
}
|
||||||
|
s := strings.Split(l, sep)
|
||||||
|
i := make([]int, len(s))
|
||||||
|
for idx := range s {
|
||||||
|
if i[idx], err = strconv.Atoi(s[idx]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a multi-line string into an array of strings
|
||||||
|
func SplitLines(in string) []string {
|
||||||
|
res := []string{}
|
||||||
|
|
||||||
|
s := bufio.NewScanner(strings.NewReader(in))
|
||||||
|
for s.Scan() {
|
||||||
|
res = append(res, s.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function parses the "size" parameter of a disk specification
|
||||||
|
// and returns the size in MB. The "size" parameter defaults to GB, but
|
||||||
|
// the unit can be explicitly set with either a G (for GB) or M (for
|
||||||
|
// MB). It returns the disk size in MB.
|
||||||
|
func GetDiskSizeMB(s string) (int, error) {
|
||||||
|
if s == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
sz := len(s)
|
||||||
|
if strings.HasSuffix(s, "M") {
|
||||||
|
return strconv.Atoi(s[:sz-1])
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(s, "G") {
|
||||||
|
s = s[:sz-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 1024 * i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertMBtoGB(i int) int {
|
||||||
|
if i < 1024 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if i%1024 == 0 {
|
||||||
|
return i / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
return (i + (1024 - i%1024)) / 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disks is the type for a list of DiskConfig
|
||||||
|
type Disks []qemu.Disk
|
||||||
|
|
||||||
|
func (l *Disks) String() string {
|
||||||
|
return fmt.Sprint(*l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is used by flag to configure value from CLI
|
||||||
|
func (l *Disks) Set(value string) error {
|
||||||
|
d := qemu.Disk{}
|
||||||
|
s := strings.Split(value, ",")
|
||||||
|
for _, p := range s {
|
||||||
|
c := strings.SplitN(p, "=", 2)
|
||||||
|
switch len(c) {
|
||||||
|
case 1:
|
||||||
|
// assume it is a filename even if no file=x
|
||||||
|
d.Path = c[0]
|
||||||
|
case 2:
|
||||||
|
switch c[0] {
|
||||||
|
case "file":
|
||||||
|
d.Path = c[1]
|
||||||
|
case "size":
|
||||||
|
size, err := GetDiskSizeMB(c[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Size = size
|
||||||
|
case "format":
|
||||||
|
d.Format = c[1]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown disk config: %s", c[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*l = append(*l, d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Disks) Type() string {
|
||||||
|
return "disk"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishedPort is used by some backends to expose a VMs port on the host
|
||||||
|
type PublishedPort struct {
|
||||||
|
Guest uint16
|
||||||
|
Host uint16
|
||||||
|
Protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublishedPort parses a string of the form <host>:<guest>[/<tcp|udp>] and returns a PublishedPort structure
|
||||||
|
func NewPublishedPort(publish string) (PublishedPort, error) {
|
||||||
|
p := PublishedPort{}
|
||||||
|
slice := strings.Split(publish, ":")
|
||||||
|
|
||||||
|
if len(slice) < 2 {
|
||||||
|
return p, fmt.Errorf("Unable to parse the ports to be published, should be in format <host>:<guest> or <host>:<guest>/<tcp|udp>")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort, err := strconv.ParseUint(slice[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return p, fmt.Errorf("The provided hostPort can't be converted to uint16")
|
||||||
|
}
|
||||||
|
|
||||||
|
right := strings.Split(slice[1], "/")
|
||||||
|
|
||||||
|
protocol := "tcp"
|
||||||
|
if len(right) == 2 {
|
||||||
|
protocol = strings.TrimSpace(strings.ToLower(right[1]))
|
||||||
|
}
|
||||||
|
if protocol != "tcp" && protocol != "udp" {
|
||||||
|
return p, fmt.Errorf("Provided protocol is not valid, valid options are: udp and tcp")
|
||||||
|
}
|
||||||
|
|
||||||
|
guestPort, err := strconv.ParseUint(right[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return p, fmt.Errorf("The provided guestPort can't be converted to uint16")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostPort < 1 || hostPort > 65535 {
|
||||||
|
return p, fmt.Errorf("Invalid hostPort: %d", hostPort)
|
||||||
|
}
|
||||||
|
if guestPort < 1 || guestPort > 65535 {
|
||||||
|
return p, fmt.Errorf("Invalid guestPort: %d", guestPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Guest = uint16(guestPort)
|
||||||
|
p.Host = uint16(hostPort)
|
||||||
|
p.Protocol = protocol
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialSSH(server, user, password string) (*ssh.Client, error) {
|
||||||
|
c, err := ssh.Dial("tcp", server+":22", &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Auth: []ssh.AuthMethod{ssh.Password(password)},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialSSHWithTimeout(server, user, password string, timeout time.Duration) (*ssh.Client, error) {
|
||||||
|
t := time.NewTimer(timeout)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return nil, fmt.Errorf("timeout while trying to connect to the server")
|
||||||
|
default:
|
||||||
|
c, err := dialSSH(server, user, password)
|
||||||
|
if err == nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgressReader(r io.Reader) *pw {
|
||||||
|
return &pw{r: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pw struct {
|
||||||
|
r io.Reader
|
||||||
|
total int
|
||||||
|
size int
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pw) Read(buf []byte) (int, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.total += len(buf)
|
||||||
|
p.mu.Unlock()
|
||||||
|
return p.r.Read(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pw) Progress() int {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
return p.total
|
||||||
|
}
|
351
cmd/d2vm/run/vbox.go
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.linka.cloud/console"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu_img"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
VboxCmd = &cobra.Command{
|
||||||
|
Use: "vbox [options] image-path",
|
||||||
|
Short: "Run the virtual machine image with Virtualbox",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: Vbox,
|
||||||
|
}
|
||||||
|
|
||||||
|
vboxmanageFlag string
|
||||||
|
name string
|
||||||
|
networks VBNetworks
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flags := VboxCmd.Flags()
|
||||||
|
// Display flags
|
||||||
|
flags.Bool("gui", false, "Show the VM GUI")
|
||||||
|
|
||||||
|
// vbox options
|
||||||
|
flags.StringVar(&vboxmanageFlag, "vboxmanage", "VBoxManage", "VBoxManage binary to use")
|
||||||
|
flags.StringVar(&name, "name", "d2vm", "Name of the Virtualbox VM")
|
||||||
|
|
||||||
|
// Paths and settings for disks
|
||||||
|
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=raw]")
|
||||||
|
|
||||||
|
// VM configuration
|
||||||
|
flags.Uint("cpus", 1, "Number of CPUs")
|
||||||
|
flags.Uint("mem", 1024, "Amount of memory in MB")
|
||||||
|
|
||||||
|
// networking
|
||||||
|
flags.Var(&networks, "networking", "Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>]")
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
log.Fatalf("TODO: Windows is not yet supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Vbox(cmd *cobra.Command, args []string) {
|
||||||
|
path := args[0]
|
||||||
|
if err := vbox(cmd.Context(), path); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vbox(ctx context.Context, path string) error {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vboxmanage, err := exec.LookPath(vboxmanageFlag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err)
|
||||||
|
}
|
||||||
|
i, err := qemu_img.Info(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get image info: %v", err)
|
||||||
|
}
|
||||||
|
if i.Format != "vdi" {
|
||||||
|
logrus.Warnf("image format is %s, expected vdi", i.Format)
|
||||||
|
vdi := filepath.Join(os.TempDir(), "d2vm", "run", filepath.Base(path)+".vdi")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(vdi), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(vdi)
|
||||||
|
logrus.Infof("converting image to raw: %s", vdi)
|
||||||
|
if err := qemu_img.Convert(ctx, "vdi", path, vdi); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path = vdi
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove machine in case it already exists
|
||||||
|
cleanup(vboxmanage, name, false)
|
||||||
|
|
||||||
|
_, out, err := manage(vboxmanage, "createvm", "--name", name, "--register")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("createvm error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--acpi", "on")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --acpi error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--memory", fmt.Sprintf("%d", mem))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --memory error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--cpus", fmt.Sprintf("%d", cpus))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --cpus error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--firmware", "bios")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --firmware error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up serial console
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--uart1", "0x3F8", "4")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --uart error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
consolePath := filepath.Join(os.TempDir(), "d2vm-vb", name, "console")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(consolePath), os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("mkir %s: %v", consolePath, err)
|
||||||
|
}
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
consolePath, err = filepath.Abs(consolePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Bad path: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO use a named pipe on Windows
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--uartmode1", "client", consolePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --uartmode error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "IDE Controller", "--add", "ide")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("storagectl error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("storageattach error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "disk")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --boot error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disks) > 0 {
|
||||||
|
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "SATA", "--add", "sata")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("storagectl error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range disks {
|
||||||
|
id := strconv.Itoa(i)
|
||||||
|
if d.Size != 0 && d.Format == "" {
|
||||||
|
d.Format = "raw"
|
||||||
|
}
|
||||||
|
if d.Format != "raw" && d.Path == "" {
|
||||||
|
return fmt.Errorf("vbox currently can only create raw disks")
|
||||||
|
}
|
||||||
|
if d.Path == "" && d.Size == 0 {
|
||||||
|
return fmt.Errorf("please specify an existing disk file or a size")
|
||||||
|
}
|
||||||
|
if d.Path == "" {
|
||||||
|
d.Path = "disk" + id + ".img"
|
||||||
|
if err := os.Truncate(d.Path, int64(d.Size)*int64(1048576)); err != nil {
|
||||||
|
return fmt.Errorf("Cannot create disk: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", d.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("storageattach error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range networks {
|
||||||
|
nic := i + 1
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nictype%d", nic), "virtio")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --nictype error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nic%d", nic), d.Type)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --nic error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
if d.Type == "hostonly" {
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--hostonlyadapter%d", nic), d.Adapter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --hostonlyadapter error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
} else if d.Type == "bridged" {
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--bridgeadapter%d", nic), d.Adapter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --bridgeadapter error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--cableconnected%d", nic), "on")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modifyvm --cableconnected error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create socket
|
||||||
|
_ = os.Remove(consolePath)
|
||||||
|
ln, err := net.Listen("unix", consolePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot listen on console socket %s: %v", consolePath, err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
var vmType string
|
||||||
|
if enableGUI {
|
||||||
|
vmType = "gui"
|
||||||
|
} else {
|
||||||
|
vmType = "headless"
|
||||||
|
}
|
||||||
|
|
||||||
|
term := console.Current()
|
||||||
|
ws, err := term.Size()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get term size: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err = manage(vboxmanage, "startvm", name, "--type", vmType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("startvm error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
defer cleanup(vboxmanage, name)
|
||||||
|
|
||||||
|
if err := term.Resize(ws); err != nil && !errors.Is(err, console.ErrUnsupported) {
|
||||||
|
return fmt.Errorf("resize term: %v", err)
|
||||||
|
}
|
||||||
|
if err := term.SetRaw(); err != nil {
|
||||||
|
return fmt.Errorf("set raw term: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := term.Reset(); err != nil {
|
||||||
|
log.Errorf("failed to reset term: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
socket, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Accept error: %v", err)
|
||||||
|
}
|
||||||
|
defer socket.Close()
|
||||||
|
errs := make(chan error, 2)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(socket, term)
|
||||||
|
errs <- err
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(term, socket)
|
||||||
|
errs <- err
|
||||||
|
}()
|
||||||
|
return <-errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanup(vboxmanage string, name string, logErrs ...bool) {
|
||||||
|
logErr := true
|
||||||
|
if len(logErrs) > 0 {
|
||||||
|
logErr = logErrs[0]
|
||||||
|
}
|
||||||
|
if _, _, err := manage(vboxmanage, "controlvm", name, "poweroff"); err != nil && logErr {
|
||||||
|
log.Errorf("controlvm poweroff error: %v", err)
|
||||||
|
}
|
||||||
|
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", "emptydrive")
|
||||||
|
if err != nil && logErr {
|
||||||
|
log.Errorf("storageattach error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
for i := range disks {
|
||||||
|
id := strconv.Itoa(i)
|
||||||
|
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", "emptydrive")
|
||||||
|
if err != nil && logErr {
|
||||||
|
log.Errorf("storageattach error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, out, err = manage(vboxmanage, "unregistervm", name, "--delete"); err != nil && logErr {
|
||||||
|
log.Errorf("unregistervm error: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func manage(vboxmanage string, args ...string) (string, string, error) {
|
||||||
|
log.Debugf("$ %s %s", vboxmanage, strings.Join(args, " "))
|
||||||
|
cmd := exec.Command(vboxmanage, args...)
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = io.MultiWriter(&stdout, logrus.StandardLogger().WriterLevel(logrus.DebugLevel))
|
||||||
|
cmd.Stderr = io.MultiWriter(&stderr, logrus.StandardLogger().WriterLevel(logrus.DebugLevel))
|
||||||
|
err := cmd.Run()
|
||||||
|
return stdout.String(), stderr.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VBNetwork is the config for a Virtual Box network
|
||||||
|
type VBNetwork struct {
|
||||||
|
Type string
|
||||||
|
Adapter string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VBNetworks is the type for a list of VBNetwork
|
||||||
|
type VBNetworks []VBNetwork
|
||||||
|
|
||||||
|
func (l *VBNetworks) String() string {
|
||||||
|
return fmt.Sprint(*l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *VBNetworks) Type() string {
|
||||||
|
return "vbnetworks"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is used by flag to configure value from CLI
|
||||||
|
func (l *VBNetworks) Set(value string) error {
|
||||||
|
d := VBNetwork{}
|
||||||
|
s := strings.Split(value, ",")
|
||||||
|
for _, p := range s {
|
||||||
|
c := strings.SplitN(p, "=", 2)
|
||||||
|
switch len(c) {
|
||||||
|
case 1:
|
||||||
|
d.Type = c[0]
|
||||||
|
case 2:
|
||||||
|
switch c[0] {
|
||||||
|
case "type":
|
||||||
|
d.Type = c[1]
|
||||||
|
case "adapter", "bridgeadapter", "hostadapter":
|
||||||
|
d.Adapter = c[1]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown network config: %s", c[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*l = append(*l, d)
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,3 +1,17 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
89
config.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configUbuntu = Config{
|
||||||
|
Kernel: "/boot/vmlinuz",
|
||||||
|
Initrd: "/boot/initrd.img",
|
||||||
|
}
|
||||||
|
configDebian = Config{
|
||||||
|
Kernel: "/boot/vmlinuz",
|
||||||
|
Initrd: "/boot/initrd.img",
|
||||||
|
}
|
||||||
|
configAlpine = Config{
|
||||||
|
Kernel: "/boot/vmlinuz-virt",
|
||||||
|
Initrd: "/boot/initramfs-virt",
|
||||||
|
}
|
||||||
|
configCentOS = Config{
|
||||||
|
Kernel: "/boot/vmlinuz",
|
||||||
|
Initrd: "/boot/initrd.img",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Root interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootUUID string
|
||||||
|
|
||||||
|
func (r RootUUID) String() string {
|
||||||
|
return "UUID=" + string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootPath string
|
||||||
|
|
||||||
|
func (r RootPath) String() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Kernel string
|
||||||
|
Initrd string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Cmdline(root Root, args ...string) string {
|
||||||
|
var r string
|
||||||
|
if root != nil {
|
||||||
|
r = fmt.Sprintf("root=%s", root.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", c.Initrd, r, strings.Join(args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r OSRelease) Config() (Config, error) {
|
||||||
|
switch r.ID {
|
||||||
|
case ReleaseUbuntu:
|
||||||
|
if r.VersionID < "20.04" {
|
||||||
|
return configDebian, nil
|
||||||
|
}
|
||||||
|
return configUbuntu, nil
|
||||||
|
case ReleaseDebian:
|
||||||
|
return configDebian, nil
|
||||||
|
case ReleaseKali:
|
||||||
|
return configDebian, nil
|
||||||
|
case ReleaseAlpine:
|
||||||
|
return configAlpine, nil
|
||||||
|
case ReleaseCentOS:
|
||||||
|
return configCentOS, nil
|
||||||
|
default:
|
||||||
|
return Config{}, fmt.Errorf("%s: distribution not supported", r.ID)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
161
config_test.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfig(t *testing.T, ctx context.Context, name, img string, config Config, luks, grubBIOS, grubEFI bool) {
|
||||||
|
require.NoError(t, docker.Pull(ctx, Arch, img))
|
||||||
|
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(name))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpPath, 0755))
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
logrus.Infof("inspecting image %s", img)
|
||||||
|
r, err := FetchDockerImageOSRelease(ctx, img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer docker.Remove(ctx, img)
|
||||||
|
if !r.SupportsLUKS() && luks {
|
||||||
|
t.Skipf("LUKS not supported for %s", r.Version)
|
||||||
|
}
|
||||||
|
d, err := NewDockerfile(r, img, "root", "", luks, grubBIOS, grubEFI)
|
||||||
|
require.NoError(t, err)
|
||||||
|
logrus.Infof("docker image based on %s", d.Release.Name)
|
||||||
|
p := filepath.Join(tmpPath, docker.FormatImgName(name))
|
||||||
|
dir := filepath.Dir(p)
|
||||||
|
f, err := os.Create(p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
require.NoError(t, d.Render(f))
|
||||||
|
imgUUID := uuid.New().String()
|
||||||
|
logrus.Infof("building kernel enabled image")
|
||||||
|
require.NoError(t, docker.Build(ctx, false, imgUUID, p, dir, Arch))
|
||||||
|
defer docker.Remove(ctx, imgUUID)
|
||||||
|
// we don't need to test the kernel location if grub is enabled
|
||||||
|
if grubBIOS || grubEFI {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel))
|
||||||
|
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
image string
|
||||||
|
config Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
image: "ubuntu:18.04",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:20.04",
|
||||||
|
config: configUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:22.04",
|
||||||
|
config: configUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:latest",
|
||||||
|
config: configUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:9",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:10",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:11",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:latest",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "kalilinux/kali-rolling:latest",
|
||||||
|
config: configDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "alpine:3.16",
|
||||||
|
config: configAlpine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "alpine",
|
||||||
|
config: configAlpine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "centos:8",
|
||||||
|
config: configCentOS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "centos:latest",
|
||||||
|
config: configCentOS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "quay.io/centos/centos:stream9",
|
||||||
|
config: configCentOS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
exec.SetDebug(true)
|
||||||
|
|
||||||
|
names := []string{"luks", "grub-bios", "grub-efi"}
|
||||||
|
bools := []bool{false, true}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.image, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, luks := range bools {
|
||||||
|
for _, grubBIOS := range bools {
|
||||||
|
for _, grubEFI := range bools {
|
||||||
|
luks := luks
|
||||||
|
grubBIOS := grubBIOS
|
||||||
|
grubEFI := grubEFI
|
||||||
|
n := []string{test.image}
|
||||||
|
for i, v := range []bool{luks, grubBIOS, grubEFI} {
|
||||||
|
if v {
|
||||||
|
n = append(n, names[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := strings.Join(n, "-")
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
testConfig(t, ctx, name, test.image, test.config, luks, grubBIOS, grubEFI)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
67
container_disk.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu_img"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk-workflow-example
|
||||||
|
uid = 107
|
||||||
|
containerDiskDockerfile = `FROM scratch
|
||||||
|
|
||||||
|
ADD --chown=%[1]d:%[1]d %[2]s /disk/
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeContainerDisk(ctx context.Context, path string, tag string, platform string) error {
|
||||||
|
tmpPath := filepath.Join(os.TempDir(), "d2vm", uuid.New().String())
|
||||||
|
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := os.RemoveAll(tmpPath); err != nil {
|
||||||
|
logrus.Errorf("failed to remove tmp dir %s: %v", tmpPath, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// convert may not be needed, but this will also copy the file in the tmp dir
|
||||||
|
qcow2 := filepath.Join(tmpPath, "disk.qcow2")
|
||||||
|
if err := qemu_img.Convert(ctx, "qcow2", path, qcow2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
disk := filepath.Base(qcow2)
|
||||||
|
dockerfileContent := fmt.Sprintf(containerDiskDockerfile, uid, disk)
|
||||||
|
dockerfile := filepath.Join(tmpPath, "Dockerfile")
|
||||||
|
if err := os.WriteFile(dockerfile, []byte(dockerfileContent), os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("failed to write dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
if err := docker.Build(ctx, false, tag, dockerfile, tmpPath, platform); err != nil {
|
||||||
|
return fmt.Errorf("failed to build container disk: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
95
convert.go
@ -17,17 +17,22 @@ package d2vm
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/svenwiltink/sparsecat"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/docker"
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Convert(ctx context.Context, img string, size int64, password string, output string, format string) error {
|
func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
|
||||||
|
o := &convertOptions{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
imgUUID := uuid.New().String()
|
imgUUID := uuid.New().String()
|
||||||
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
||||||
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
||||||
@ -36,55 +41,65 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu
|
|||||||
defer os.RemoveAll(tmpPath)
|
defer os.RemoveAll(tmpPath)
|
||||||
|
|
||||||
logrus.Infof("inspecting image %s", img)
|
logrus.Infof("inspecting image %s", img)
|
||||||
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
|
r, err := FetchDockerImageOSRelease(ctx, img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d, err := NewDockerfile(r, img, password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logrus.Infof("docker image based on %s", d.Release.Name)
|
|
||||||
p := filepath.Join(tmpPath, docker.FormatImgName(img))
|
|
||||||
dir := filepath.Dir(p)
|
|
||||||
f, err := os.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
if err := d.Render(f); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logrus.Infof("building kernel enabled image")
|
|
||||||
if err := docker.Cmd(ctx, "image", "build", "-t", imgUUID, "-f", p, dir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer docker.Cmd(ctx, "image", "rm", imgUUID)
|
|
||||||
archive := imgUUID + ".tar"
|
|
||||||
archivePath := filepath.Join(tmpPath, archive)
|
|
||||||
logrus.Infof("creating root file system archive")
|
|
||||||
if err := docker.Cmd(ctx, "run", "-d", "--name", imgUUID, imgUUID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := docker.Cmd(ctx, "export", "--output", archivePath, imgUUID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := docker.Cmd(ctx, "rm", "-f", imgUUID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logrus.Infof("creating vm image")
|
|
||||||
|
|
||||||
b, err := NewBuilder(tmpPath, archivePath, "", size, r, format)
|
if o.luksPassword != "" && !r.SupportsLUKS() {
|
||||||
|
return fmt.Errorf("luks is not supported for %s %s", r.Name, r.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.raw {
|
||||||
|
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.hasGrubBIOS(), o.hasGrubEFI())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("docker image based on %s %s", d.Release.Name, d.Release.Version)
|
||||||
|
p := filepath.Join(tmpPath, docker.FormatImgName(img))
|
||||||
|
dir := filepath.Dir(p)
|
||||||
|
f, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := d.Render(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("building kernel enabled image")
|
||||||
|
if err := docker.Build(ctx, o.pull, imgUUID, p, dir, o.platform); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !o.keepCache {
|
||||||
|
defer docker.Remove(ctx, imgUUID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// for raw images, we just tag the image with the uuid
|
||||||
|
if err := docker.Tag(ctx, img, imgUUID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !o.keepCache {
|
||||||
|
defer docker.Remove(ctx, imgUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("creating vm image")
|
||||||
|
format := strings.TrimPrefix(filepath.Ext(o.output), ".")
|
||||||
|
if format == "" {
|
||||||
|
format = "raw"
|
||||||
|
}
|
||||||
|
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.luksPassword, o.bootLoader, o.platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer b.Close()
|
||||||
if err := b.Build(ctx); err != nil {
|
if err := b.Build(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll(output); err != nil {
|
if err := os.RemoveAll(o.output); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := MoveFile(filepath.Join(tmpPath, "disk0.qcow2"), output); err != nil {
|
if err := MoveFile(filepath.Join(tmpPath, "disk0."+format), o.output); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -101,7 +116,7 @@ func MoveFile(sourcePath, destPath string) error {
|
|||||||
return fmt.Errorf("failed to open dest file: %s", err)
|
return fmt.Errorf("failed to open dest file: %s", err)
|
||||||
}
|
}
|
||||||
defer outputFile.Close()
|
defer outputFile.Close()
|
||||||
_, err = io.Copy(outputFile, inputFile)
|
_, err = sparsecat.NewDecoder(sparsecat.NewEncoder(inputFile)).WriteTo(outputFile)
|
||||||
inputFile.Close()
|
inputFile.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write to output file: %s", err)
|
return fmt.Errorf("failed to write to output file: %s", err)
|
||||||
|
129
convert_options.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
type ConvertOption func(o *convertOptions)
|
||||||
|
|
||||||
|
type convertOptions struct {
|
||||||
|
size uint64
|
||||||
|
password string
|
||||||
|
output string
|
||||||
|
cmdLineExtra string
|
||||||
|
networkManager NetworkManager
|
||||||
|
bootLoader string
|
||||||
|
raw bool
|
||||||
|
|
||||||
|
splitBoot bool
|
||||||
|
bootSize uint64
|
||||||
|
bootFS BootFS
|
||||||
|
|
||||||
|
luksPassword string
|
||||||
|
|
||||||
|
keepCache bool
|
||||||
|
platform string
|
||||||
|
pull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *convertOptions) hasGrubBIOS() bool {
|
||||||
|
return o.bootLoader == "grub" || o.bootLoader == "grub-bios"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *convertOptions) hasGrubEFI() bool {
|
||||||
|
return o.bootLoader == "grub" || o.bootLoader == "grub-efi"
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSize(size uint64) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPassword(password string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithOutput(output string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.output = output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCmdLineExtra(cmdLineExtra string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.cmdLineExtra = cmdLineExtra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNetworkManager(networkManager NetworkManager) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.networkManager = networkManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBootLoader(bootLoader string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.bootLoader = bootLoader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRaw(raw bool) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.raw = raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSplitBoot(b bool) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.splitBoot = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBootSize(bootSize uint64) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.bootSize = bootSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBootFS(bootFS BootFS) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.bootFS = bootFS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLuksPassword(password string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.luksPassword = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithKeepCache(b bool) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.keepCache = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlatform(platform string) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.platform = platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPull(b bool) ConvertOption {
|
||||||
|
return func(o *convertOptions) {
|
||||||
|
o.pull = b
|
||||||
|
}
|
||||||
|
}
|
126
docker_image.go
@ -15,58 +15,122 @@
|
|||||||
package d2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/cmd/crane/cmd"
|
||||||
|
"github.com/google/go-containerregistry/pkg/crane"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dockerImageRun = `
|
dockerImageRun = `
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
{{- range .Config.Env }}
|
{{- range .DockerImageConfig.Env }}
|
||||||
export {{ . }}
|
export {{ . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
cd {{- if .Config.WorkingDir }}{{ .Config.WorkingDir }}{{- else }}/{{- end }}
|
{{ if .DockerImageConfig.WorkingDir }}cd {{ .DockerImageConfig.WorkingDir }}{{ end }}
|
||||||
|
|
||||||
{{ .Config.Entrypoint }} {{ .Config.Args }}
|
{{ if .DockerImageConfig.User }}su {{ .DockerImageConfig.User }} -p -s /bin/sh -c '{{ end }}{{ if .DockerImageConfig.Entrypoint}}{{ format .DockerImageConfig.Entrypoint }} {{ end}}{{ if .DockerImageConfig.Cmd }}{{ format .DockerImageConfig.Cmd }}{{ end }}{{ if .DockerImageConfig.User }}'{{- end }}
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Parse(dockerImageRun))
|
dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Funcs(map[string]interface{}{"format": func(a []string) string {
|
||||||
|
var o []string
|
||||||
|
for _, v := range a {
|
||||||
|
o = append(o, fmt.Sprintf("\"%s\"", v))
|
||||||
|
}
|
||||||
|
return strings.Join(o, " ")
|
||||||
|
}}).Parse(dockerImageRun))
|
||||||
|
|
||||||
|
_ = cmd.NewCmdFlatten
|
||||||
)
|
)
|
||||||
|
|
||||||
type DockerImage struct {
|
type DockerImage struct {
|
||||||
Config struct {
|
DockerImageConfig `json:"Config"`
|
||||||
Hostname string `json:"Hostname"`
|
Architecture string `json:"Architecture"`
|
||||||
Domainname string `json:"Domainname"`
|
Os string `json:"Os"`
|
||||||
User string `json:"User"`
|
Size int `json:"Size"`
|
||||||
AttachStdin bool `json:"AttachStdin"`
|
}
|
||||||
AttachStdout bool `json:"AttachStdout"`
|
|
||||||
AttachStderr bool `json:"AttachStderr"`
|
type DockerImageConfig struct {
|
||||||
ExposedPorts struct {
|
Image string `json:"Image"`
|
||||||
Tcp struct {
|
Hostname string `json:"Hostname"`
|
||||||
} `json:"3000/tcp"`
|
Domainname string `json:"Domainname"`
|
||||||
} `json:"ExposedPorts"`
|
User string `json:"User"`
|
||||||
Tty bool `json:"Tty"`
|
Env []string `json:"Env"`
|
||||||
OpenStdin bool `json:"OpenStdin"`
|
Cmd []string `json:"Cmd"`
|
||||||
StdinOnce bool `json:"StdinOnce"`
|
WorkingDir string `json:"WorkingDir"`
|
||||||
Env []string `json:"Env"`
|
Entrypoint []string `json:"Entrypoint"`
|
||||||
Cmd []string `json:"Cmd"`
|
|
||||||
Image string `json:"Image"`
|
|
||||||
Volumes interface{} `json:"Volumes"`
|
|
||||||
WorkingDir string `json:"WorkingDir"`
|
|
||||||
Entrypoint []string `json:"Entrypoint"`
|
|
||||||
OnBuild interface{} `json:"OnBuild"`
|
|
||||||
Labels interface{} `json:"Labels"`
|
|
||||||
} `json:"Config"`
|
|
||||||
Architecture string `json:"Architecture"`
|
|
||||||
Os string `json:"Os"`
|
|
||||||
Size int `json:"Size"`
|
|
||||||
VirtualSize int `json:"VirtualSize"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i DockerImage) AsRunScript(w io.Writer) error {
|
func (i DockerImage) AsRunScript(w io.Writer) error {
|
||||||
return dockerImageRunTemplate.Execute(w, i)
|
return dockerImageRunTemplate.Execute(w, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
|
||||||
|
if err := os.MkdirAll(imageTmpPath, os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// save the image to a tar file to avoid loading it in memory
|
||||||
|
tar := filepath.Join(imageTmpPath, "img.layers.tar")
|
||||||
|
if err := docker.ImageSave(ctx, tag, tar); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
img, err := crane.Load(tar)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i := &image{
|
||||||
|
img: img,
|
||||||
|
dir: imageTmpPath,
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type image struct {
|
||||||
|
tag string
|
||||||
|
img v1.Image
|
||||||
|
dir string
|
||||||
|
Config string `json:"Config"`
|
||||||
|
RepoTags []string `json:"RepoTags"`
|
||||||
|
Layers []string `json:"Layers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i image) Flatten(ctx context.Context, out string) error {
|
||||||
|
if err := os.MkdirAll(out, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tar := filepath.Join(i.dir, "img.tar")
|
||||||
|
f, err := os.Create(tar)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.Copy(f, mutate.Extract(i.img)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := exec.Run(ctx, "tar", "xvf", tar, "-C", out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i image) Close() error {
|
||||||
|
return os.RemoveAll(i.dir)
|
||||||
|
}
|
||||||
|
168
docker_image_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDockerImageAsRunSript(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
image DockerImage
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nothing",
|
||||||
|
image: DockerImage{
|
||||||
|
DockerImageConfig: DockerImageConfig{
|
||||||
|
User: "",
|
||||||
|
WorkingDir: "",
|
||||||
|
Env: nil,
|
||||||
|
Entrypoint: nil,
|
||||||
|
Cmd: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tail -f /dev/null",
|
||||||
|
image: DockerImage{
|
||||||
|
DockerImageConfig: DockerImageConfig{
|
||||||
|
User: "root",
|
||||||
|
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tail -f /dev/null inside home",
|
||||||
|
image: DockerImage{
|
||||||
|
DockerImageConfig: DockerImageConfig{
|
||||||
|
User: "root",
|
||||||
|
WorkingDir: "/root",
|
||||||
|
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd /root
|
||||||
|
|
||||||
|
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subshell tail -f /dev/null",
|
||||||
|
image: DockerImage{
|
||||||
|
DockerImageConfig: DockerImageConfig{
|
||||||
|
User: "root",
|
||||||
|
Entrypoint: []string{"/bin/sh", "-c"},
|
||||||
|
Cmd: []string{"tail -f /dev/null"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
su root -p -s /bin/sh -c '"/bin/sh" "-c" "tail -f /dev/null"'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "www-data with env",
|
||||||
|
image: DockerImage{
|
||||||
|
DockerImageConfig: DockerImageConfig{
|
||||||
|
User: "www-data",
|
||||||
|
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||||
|
Env: []string{"ENV=PROD", "DB=mysql://user:password@localhost"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
#!/bin/sh
|
||||||
|
export ENV=PROD
|
||||||
|
export DB=mysql://user:password@localhost
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
su www-data -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var w bytes.Buffer
|
||||||
|
require.NoError(t, tt.image.AsRunScript(&w))
|
||||||
|
assert.Equal(t, tt.want, w.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageFlatten(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
img = "d2vm-flatten-test"
|
||||||
|
dockerfile = `FROM alpine
|
||||||
|
|
||||||
|
COPY resolv.conf /etc/
|
||||||
|
COPY hostname /etc/
|
||||||
|
|
||||||
|
RUN rm -rf /etc/apk
|
||||||
|
`
|
||||||
|
)
|
||||||
|
exec.SetDebug(true)
|
||||||
|
tmp := filepath.Join(os.TempDir(), "d2vm-tests", "image-flatten")
|
||||||
|
require.NoError(t, os.MkdirAll(tmp, os.ModePerm))
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(tmp, "hostname"), []byte("d2vm-flatten-test"), perm))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(tmp, "resolv.conf"), []byte("nameserver 8.8.8.8"), perm))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
|
||||||
|
require.NoError(t, docker.Build(ctx, false, img, "", tmp, "linux/amd64"))
|
||||||
|
defer docker.Remove(ctx, img)
|
||||||
|
|
||||||
|
imgTmp := filepath.Join(tmp, "image")
|
||||||
|
|
||||||
|
i, err := NewImage(ctx, img, imgTmp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rootfs := filepath.Join(tmp, "rootfs")
|
||||||
|
require.NoError(t, i.Flatten(ctx, rootfs))
|
||||||
|
|
||||||
|
b, err := os.ReadFile(filepath.Join(rootfs, "etc", "resolv.conf"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "nameserver 8.8.8.8", string(b))
|
||||||
|
|
||||||
|
b, err = os.ReadFile(filepath.Join(rootfs, "etc", "hostname"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "d2vm-flatten-test", string(b))
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(rootfs, "etc", "apk"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, i.Close())
|
||||||
|
_, err = os.Stat(imgTmp)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
@ -18,7 +18,10 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/ubuntu.Dockerfile
|
//go:embed templates/ubuntu.Dockerfile
|
||||||
@ -34,39 +37,92 @@ var alpineDockerfile string
|
|||||||
var centOSDockerfile string
|
var centOSDockerfile string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Parse(ubuntuDockerfile))
|
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile))
|
||||||
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Parse(debianDockerfile))
|
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile))
|
||||||
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Parse(alpineDockerfile))
|
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile))
|
||||||
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Parse(centOSDockerfile))
|
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NetworkManager string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NetworkManagerNone NetworkManager = "none"
|
||||||
|
NetworkManagerIfupdown2 NetworkManager = "ifupdown"
|
||||||
|
NetworkManagerNetplan NetworkManager = "netplan"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n NetworkManager) Validate() error {
|
||||||
|
switch n {
|
||||||
|
case NetworkManagerNone, NetworkManagerIfupdown2, NetworkManagerNetplan:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported network manager: %s", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Dockerfile struct {
|
type Dockerfile struct {
|
||||||
Image string
|
Image string
|
||||||
Password string
|
Password string
|
||||||
Release OSRelease
|
Release OSRelease
|
||||||
tmpl *template.Template
|
NetworkManager NetworkManager
|
||||||
|
Luks bool
|
||||||
|
GrubBIOS bool
|
||||||
|
GrubEFI bool
|
||||||
|
tmpl *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dockerfile) Grub() bool {
|
||||||
|
return d.GrubBIOS || d.GrubEFI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dockerfile) Render(w io.Writer) error {
|
func (d Dockerfile) Render(w io.Writer) error {
|
||||||
return d.tmpl.Execute(w, d)
|
return d.tmpl.Execute(w, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error) {
|
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grubBIOS, grubEFI bool) (Dockerfile, error) {
|
||||||
if password == "" {
|
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, GrubBIOS: grubBIOS, GrubEFI: grubEFI}
|
||||||
password = "root"
|
var net NetworkManager
|
||||||
}
|
|
||||||
d := Dockerfile{Release: release, Image: img, Password: password}
|
|
||||||
switch release.ID {
|
switch release.ID {
|
||||||
case ReleaseDebian:
|
case ReleaseDebian:
|
||||||
d.tmpl = debianDockerfileTemplate
|
d.tmpl = debianDockerfileTemplate
|
||||||
|
net = NetworkManagerIfupdown2
|
||||||
|
case ReleaseKali:
|
||||||
|
d.tmpl = debianDockerfileTemplate
|
||||||
|
net = NetworkManagerIfupdown2
|
||||||
case ReleaseUbuntu:
|
case ReleaseUbuntu:
|
||||||
d.tmpl = ubuntuDockerfileTemplate
|
d.tmpl = ubuntuDockerfileTemplate
|
||||||
|
if release.VersionID < "18.04" {
|
||||||
|
net = NetworkManagerIfupdown2
|
||||||
|
} else {
|
||||||
|
net = NetworkManagerNetplan
|
||||||
|
}
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
d.tmpl = alpineDockerfileTemplate
|
d.tmpl = alpineDockerfileTemplate
|
||||||
|
net = NetworkManagerIfupdown2
|
||||||
|
if networkManager == NetworkManagerNetplan {
|
||||||
|
return d, fmt.Errorf("netplan is not supported on alpine")
|
||||||
|
}
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS:
|
||||||
d.tmpl = centOSDockerfileTemplate
|
d.tmpl = centOSDockerfileTemplate
|
||||||
|
net = NetworkManagerNone
|
||||||
|
if networkManager != "" && networkManager != NetworkManagerNone {
|
||||||
|
return Dockerfile{}, fmt.Errorf("network manager is not supported on centos")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
|
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
|
||||||
}
|
}
|
||||||
|
if d.NetworkManager == "" {
|
||||||
|
if release.ID != ReleaseCentOS {
|
||||||
|
logrus.Warnf("no network manager specified, using distribution defaults: %s", net)
|
||||||
|
}
|
||||||
|
d.NetworkManager = net
|
||||||
|
}
|
||||||
|
if err := d.NetworkManager.Validate(); err != nil {
|
||||||
|
return Dockerfile{}, err
|
||||||
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tplFuncs = template.FuncMap{
|
||||||
|
"atoi": strconv.Atoi,
|
||||||
|
}
|
||||||
|
1
docs/CNAME
Normal file
@ -0,0 +1 @@
|
|||||||
|
d2vm.linka.cloud
|
BIN
docs/content/assets/d2vm-dark-b.png
Normal file
After Width: | Height: | Size: 223 KiB |
BIN
docs/content/assets/d2vm-dark-tr.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
docs/content/assets/d2vm-dark.png
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
docs/content/assets/d2vm-favicon.png
Normal file
After Width: | Height: | Size: 269 KiB |
BIN
docs/content/assets/d2vm-light-tr.png
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
docs/content/assets/d2vm-light.png
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
docs/content/assets/d2vm.png
Normal file
After Width: | Height: | Size: 163 KiB |
1
docs/content/assets/matrixorg-icon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 66 56" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M49.46 37.36h-5.32c-.178 0-.323-.145-.323-.323V26.06l-.074-1.808c-.047-.53-.173-.992-.376-1.376a2.2 2.2 0 0 0-.868-.883c-.38-.22-.93-.332-1.62-.332s-1.238.13-1.647.382-.743.597-.976 1a4.21 4.21 0 0 0-.486 1.462c-.085.567-.128 1.15-.128 1.732v10.8c0 .178-.145.323-.323.323H32c-.178 0-.323-.145-.323-.323V26.18l-.037-1.7c-.024-.524-.124-1.013-.297-1.45-.164-.415-.43-.74-.814-.992s-.972-.378-1.752-.378c-.22 0-.527.053-.908.157-.368.1-.732.294-1.08.577s-.65.694-.904 1.235-.382 1.27-.382 2.167v11.24c0 .178-.144.323-.323.323h-5.32c-.178 0-.323-.145-.323-.323v-19.37c0-.178.145-.322.323-.322h5.02c.178 0 .323.145.323.322v1.794c.618-.726 1.33-1.315 2.125-1.757 1.032-.574 2.225-.865 3.548-.865 1.265 0 2.44.25 3.5.743.934.44 1.68 1.17 2.224 2.18.556-.703 1.263-1.34 2.108-1.895 1.036-.682 2.274-1.028 3.68-1.028 1.048 0 2.036.13 2.937.387.917.263 1.715.7 2.373 1.267s1.18 1.348 1.548 2.278c.363.922.547 2.04.547 3.323v12.964c0 .178-.145.323-.323.323z" opacity=".5"/><path d="M24.88 17.675v2.623h.075c.7-.998 1.542-1.774 2.53-2.323s2.117-.824 3.39-.824c1.224 0 2.342.238 3.353.712s1.78 1.31 2.305 2.51c.574-.85 1.355-1.6 2.342-2.248s2.154-.974 3.504-.974c1.024 0 1.973.125 2.848.375s1.623.65 2.248 1.2 1.11 1.268 1.462 2.154.525 1.955.525 3.204v12.964h-5.32V26.07l-.075-1.836c-.05-.574-.187-1.073-.412-1.5s-.556-.762-.993-1.012-1.03-.374-1.78-.374-1.355.145-1.817.43a3.12 3.12 0 0 0-1.087 1.124c-.263.461-.437.987-.524 1.574s-.132 1.184-.131 1.78v10.79H32V26.182l-.037-1.705c-.025-.562-.13-1.08-.32-1.556s-.5-.855-.937-1.143-1.08-.43-1.93-.43c-.25 0-.58.056-.993.17a3.3 3.3 0 0 0-1.199.637c-.388.313-.718.762-.993 1.35s-.412 1.355-.412 2.304v11.24h-5.32V17.675z" opacity=".5"/><path d="M1.432 1.244v51.833h3.73v1.244H0V0h5.162v1.243zm20.788 16.43v2.623h.075c.7-.998 1.542-1.774 2.53-2.323s2.117-.824 3.4-.824c1.224 0 2.342.238 3.353.712s1.78 1.3 2.305 2.5c.574-.85 1.355-1.6 2.342-2.248s2.154-.974 3.504-.974c1.024 0 1.973.125 2.848.375s1.623.65 2.248 1.2 1.1 1.268 1.462 2.154.525 1.955.525 3.204v12.964h-5.32V26.06l-.075-1.836c-.05-.574-.187-1.073-.412-1.5s-.556-.762-.993-1.012-1.03-.374-1.78-.374-1.355.145-1.817.43a3.12 3.12 0 0 0-1.087 1.124c-.263.46-.437.987-.524 1.574s-.132 1.184-.131 1.78v10.8h-5.32V26.182l-.037-1.705c-.025-.562-.13-1.08-.32-1.556s-.5-.855-.937-1.143-1.08-.43-1.93-.43c-.25 0-.58.056-.993.17a3.3 3.3 0 0 0-1.199.637c-.388.313-.718.762-.993 1.35s-.412 1.355-.412 2.304v11.24H17.2V17.675zm40.348 35.402V1.244h-3.73V0H64v54.32h-5.162v-1.244z" fill="#000"/></g></symbol></svg>
|
After Width: | Height: | Size: 2.7 KiB |
1
docs/content/full-example.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../examples/full/README.md
|
1
docs/content/index.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../README.md
|
20
docs/content/reference/d2vm.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## d2vm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for d2vm
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm build](d2vm_build.md) - Build a vm image from Dockerfile
|
||||||
|
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
* [d2vm convert](d2vm_convert.md) - Convert Docker image to vm image
|
||||||
|
* [d2vm run](d2vm_run.md) - Run the virtual machine image
|
||||||
|
* [d2vm version](d2vm_version.md) -
|
||||||
|
|
44
docs/content/reference/d2vm_build.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
## d2vm build
|
||||||
|
|
||||||
|
Build a vm image from Dockerfile
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm build [context directory] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
||||||
|
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
|
||||||
|
--boot-size uint Size of the boot partition in MB (default 100)
|
||||||
|
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
|
||||||
|
--build-arg stringArray Set build-time variables
|
||||||
|
-f, --file string Name of the Dockerfile
|
||||||
|
--force Override output qcow2 image
|
||||||
|
-h, --help help for build
|
||||||
|
--keep-cache Keep the images after the build
|
||||||
|
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
|
||||||
|
--network-manager string Network manager to use for the image: none, netplan, ifupdown
|
||||||
|
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
|
||||||
|
-p, --password string Optional root user password
|
||||||
|
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
|
||||||
|
--pull Always pull docker image
|
||||||
|
--push Push the container disk image to the registry
|
||||||
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
--split-boot Split the boot partition from the root partition
|
||||||
|
-t, --tag string Container disk Docker image tag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm](d2vm.md) -
|
||||||
|
|
31
docs/content/reference/d2vm_completion.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## d2vm completion
|
||||||
|
|
||||||
|
Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for d2vm for the specified shell.
|
||||||
|
See each sub-command's help for details on how to use the generated script.
|
||||||
|
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for completion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm](d2vm.md) -
|
||||||
|
* [d2vm completion bash](d2vm_completion_bash.md) - Generate the autocompletion script for bash
|
||||||
|
* [d2vm completion fish](d2vm_completion_fish.md) - Generate the autocompletion script for fish
|
||||||
|
* [d2vm completion powershell](d2vm_completion_powershell.md) - Generate the autocompletion script for powershell
|
||||||
|
* [d2vm completion zsh](d2vm_completion_zsh.md) - Generate the autocompletion script for zsh
|
||||||
|
|
50
docs/content/reference/d2vm_completion_bash.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
## d2vm completion bash
|
||||||
|
|
||||||
|
Generate the autocompletion script for bash
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the bash shell.
|
||||||
|
|
||||||
|
This script depends on the 'bash-completion' package.
|
||||||
|
If it is not installed already, you can install it via your OS's package manager.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
source <(d2vm completion bash)
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
#### Linux:
|
||||||
|
|
||||||
|
d2vm completion bash > /etc/bash_completion.d/d2vm
|
||||||
|
|
||||||
|
#### macOS:
|
||||||
|
|
||||||
|
d2vm completion bash > $(brew --prefix)/etc/bash_completion.d/d2vm
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm completion bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for bash
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
41
docs/content/reference/d2vm_completion_fish.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
## d2vm completion fish
|
||||||
|
|
||||||
|
Generate the autocompletion script for fish
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the fish shell.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
d2vm completion fish | source
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
d2vm completion fish > ~/.config/fish/completions/d2vm.fish
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm completion fish [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for fish
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
38
docs/content/reference/d2vm_completion_powershell.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
## d2vm completion powershell
|
||||||
|
|
||||||
|
Generate the autocompletion script for powershell
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for powershell.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
d2vm completion powershell | Out-String | Invoke-Expression
|
||||||
|
|
||||||
|
To load completions for every new session, add the output of the above command
|
||||||
|
to your powershell profile.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm completion powershell [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for powershell
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
52
docs/content/reference/d2vm_completion_zsh.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
## d2vm completion zsh
|
||||||
|
|
||||||
|
Generate the autocompletion script for zsh
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the zsh shell.
|
||||||
|
|
||||||
|
If shell completion is not already enabled in your environment you will need
|
||||||
|
to enable it. You can execute the following once:
|
||||||
|
|
||||||
|
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
source <(d2vm completion zsh)
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
#### Linux:
|
||||||
|
|
||||||
|
d2vm completion zsh > "${fpath[1]}/_d2vm"
|
||||||
|
|
||||||
|
#### macOS:
|
||||||
|
|
||||||
|
d2vm completion zsh > $(brew --prefix)/share/zsh/site-functions/_d2vm
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm completion zsh [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for zsh
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
42
docs/content/reference/d2vm_convert.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## d2vm convert
|
||||||
|
|
||||||
|
Convert Docker image to vm image
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm convert [docker image] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
||||||
|
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
|
||||||
|
--boot-size uint Size of the boot partition in MB (default 100)
|
||||||
|
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
|
||||||
|
--force Override output qcow2 image
|
||||||
|
-h, --help help for convert
|
||||||
|
--keep-cache Keep the images after the build
|
||||||
|
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
|
||||||
|
--network-manager string Network manager to use for the image: none, netplan, ifupdown
|
||||||
|
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
|
||||||
|
-p, --password string Optional root user password
|
||||||
|
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
|
||||||
|
--pull Always pull docker image
|
||||||
|
--push Push the container disk image to the registry
|
||||||
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
|
-s, --size string The output image size (default "10G")
|
||||||
|
--split-boot Split the boot partition from the root partition
|
||||||
|
-t, --tag string Container disk Docker image tag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm](d2vm.md) -
|
||||||
|
|
24
docs/content/reference/d2vm_run.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
## d2vm run
|
||||||
|
|
||||||
|
Run the virtual machine image
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm](d2vm.md) -
|
||||||
|
* [d2vm run hetzner](d2vm_run_hetzner.md) - Run the virtual machine image on Hetzner Cloud
|
||||||
|
* [d2vm run qemu](d2vm_run_qemu.md) - Run the virtual machine image with qemu
|
||||||
|
* [d2vm run vbox](d2vm_run_vbox.md) - Run the virtual machine image with Virtualbox
|
||||||
|
|
32
docs/content/reference/d2vm_run_hetzner.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
## d2vm run hetzner
|
||||||
|
|
||||||
|
Run the virtual machine image on Hetzner Cloud
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm run hetzner [options] image-path [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for hetzner
|
||||||
|
-l, --location string d2vm server location (default "hel1-dc2")
|
||||||
|
-n, --name string d2vm server name (default "d2vm")
|
||||||
|
--rm remove server when done
|
||||||
|
-i, --ssh-key string d2vm image identity key
|
||||||
|
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
|
||||||
|
-t, --type string d2vm server type (default "cx11")
|
||||||
|
-u, --user string d2vm image ssh user (default "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm run](d2vm_run.md) - Run the virtual machine image
|
||||||
|
|
36
docs/content/reference/d2vm_run_qemu.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
## d2vm run qemu
|
||||||
|
|
||||||
|
Run the virtual machine image with qemu
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm run qemu [options] [image-path] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "kvm:tcg")
|
||||||
|
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
|
||||||
|
--bios string Path to the optional bios binary
|
||||||
|
--cpus uint Number of CPUs (default 1)
|
||||||
|
--detached Set qemu container to run in the background
|
||||||
|
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])
|
||||||
|
--gui Set qemu to use video output instead of stdio
|
||||||
|
-h, --help help for qemu
|
||||||
|
--mem uint Amount of memory in MB (default 1024)
|
||||||
|
--networking string Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.` (default "user")
|
||||||
|
--publish multiple-flag Publish a vm's port(s) to the host (default []) (default A multiple flag is a type of flag that can be repeated any number of times)
|
||||||
|
--qemu string Path to the qemu binary (otherwise look in $PATH)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm run](d2vm_run.md) - Run the virtual machine image
|
||||||
|
|
32
docs/content/reference/d2vm_run_vbox.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
## d2vm run vbox
|
||||||
|
|
||||||
|
Run the virtual machine image with Virtualbox
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm run vbox [options] image-path [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--cpus uint Number of CPUs (default 1)
|
||||||
|
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=raw] (default [])
|
||||||
|
--gui Show the VM GUI
|
||||||
|
-h, --help help for vbox
|
||||||
|
--mem uint Amount of memory in MB (default 1024)
|
||||||
|
--name string Name of the Virtualbox VM (default "d2vm")
|
||||||
|
--networking vbnetworks Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>] (default [])
|
||||||
|
--vboxmanage string VBoxManage binary to use (default "VBoxManage")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm run](d2vm_run.md) - Run the virtual machine image
|
||||||
|
|
25
docs/content/reference/d2vm_version.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
## d2vm version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
d2vm version [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
|
-v, --verbose Enable Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [d2vm](d2vm.md) -
|
||||||
|
|
43
docs/mkdocs.yml
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
site_name: ""
|
||||||
|
docs_dir: content
|
||||||
|
site_dir: build
|
||||||
|
edit_uri: edit/docs/docs/content/
|
||||||
|
theme:
|
||||||
|
name: linka-cloud
|
||||||
|
logo: assets/d2vm-light-tr.png
|
||||||
|
favicon: assets/d2vm-favicon.png
|
||||||
|
language: en
|
||||||
|
repo_url: https://github.com/linka-cloud/d2vm
|
||||||
|
copyright: Copyright © 2022 Linka Cloud
|
||||||
|
nav:
|
||||||
|
- Getting Started: index.md
|
||||||
|
- Complete Example: full-example.md
|
||||||
|
- Command Line:
|
||||||
|
- d2vm: reference/d2vm.md
|
||||||
|
- build: reference/d2vm_build.md
|
||||||
|
- convert: reference/d2vm_convert.md
|
||||||
|
- run:
|
||||||
|
- hetzner: reference/d2vm_run_hetzner.md
|
||||||
|
- qemu: reference/d2vm_run_qemu.md
|
||||||
|
- virtualbox: reference/d2vm_run_vbox.md
|
||||||
|
- completion:
|
||||||
|
- bash: reference/d2vm_completion_bash.md
|
||||||
|
- fish: reference/d2vm_completion_fish.md
|
||||||
|
- powershell: reference/d2vm_completion_powershell.md
|
||||||
|
- zsh: reference/d2vm_completion_zsh.md
|
||||||
|
- version: reference/d2vm_version.md
|
||||||
|
|
||||||
|
extra:
|
||||||
|
homepage: https://github.com/linka-cloud/d2vm
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/linka-cloud
|
||||||
|
- icon: fontawesome/brands/docker
|
||||||
|
link: https://hub.docker.com/r/linkacloud
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- pymdownx.highlight:
|
||||||
|
use_pygments: true
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tasklist
|
||||||
|
|
212
e2e/e2e_test.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
require2 "github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm"
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu"
|
||||||
|
)
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
efi bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type img struct {
|
||||||
|
name string
|
||||||
|
luks string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
images = []img{
|
||||||
|
{name: "alpine:3.17", luks: "Enter passphrase for /dev/sda2:"},
|
||||||
|
{name: "ubuntu:20.04", luks: "Please unlock disk root:"},
|
||||||
|
{name: "ubuntu:22.04", luks: "Please unlock disk root:"},
|
||||||
|
{name: "debian:10", luks: "Please unlock disk root:"},
|
||||||
|
{name: "debian:11", luks: "Please unlock disk root:"},
|
||||||
|
{name: "centos:8", luks: "Please enter passphrase for disk"},
|
||||||
|
{name: "quay.io/centos/centos:stream9", luks: "Please enter passphrase for disk"},
|
||||||
|
}
|
||||||
|
imgNames = func() []string {
|
||||||
|
var imgs []string
|
||||||
|
for _, img := range images {
|
||||||
|
imgs = append(imgs, img.name)
|
||||||
|
}
|
||||||
|
return imgs
|
||||||
|
}()
|
||||||
|
imgs = flag.String("images", "", "comma separated list of images to test, must be one of: "+strings.Join(imgNames, ","))
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvert(t *testing.T) {
|
||||||
|
require := require2.New(t)
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "single-partition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "split-boot",
|
||||||
|
args: []string{"--split-boot"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fat32",
|
||||||
|
args: []string{"--split-boot", "--boot-fs=fat32"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "luks",
|
||||||
|
args: []string{"--luks-password=root"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grub",
|
||||||
|
args: []string{"--bootloader=grub"},
|
||||||
|
efi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grub-luks",
|
||||||
|
args: []string{"--bootloader=grub", "--luks-password=root"},
|
||||||
|
efi: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testImgs []img
|
||||||
|
imgs:
|
||||||
|
for _, v := range strings.Split(*imgs, ",") {
|
||||||
|
for _, img := range images {
|
||||||
|
if img.name == v {
|
||||||
|
testImgs = append(testImgs, img)
|
||||||
|
continue imgs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("invalid image: %q, valid images: %s", v, strings.Join(imgNames, ","))
|
||||||
|
}
|
||||||
|
if len(testImgs) == 0 {
|
||||||
|
testImgs = images
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
dir := filepath.Join("/tmp", "d2vm-e2e", tt.name)
|
||||||
|
require.NoError(os.MkdirAll(dir, os.ModePerm))
|
||||||
|
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
for _, img := range testImgs {
|
||||||
|
if strings.Contains(img.name, "centos") && tt.efi {
|
||||||
|
t.Skip("efi not supported for CentOS")
|
||||||
|
}
|
||||||
|
t.Run(img.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
require := require2.New(t)
|
||||||
|
|
||||||
|
out := filepath.Join(dir, strings.NewReplacer(":", "-", ".", "-", "/", "-").Replace(img.name)+".qcow2")
|
||||||
|
|
||||||
|
if _, err := os.Stat(out); err == nil {
|
||||||
|
require.NoError(os.Remove(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(docker.RunD2VM(ctx, d2vm.Image, d2vm.Version, dir, dir, "convert", append([]string{"-p", "root", "-o", "/out/" + filepath.Base(out), "-v", "--keep-cache", img.name}, tt.args...)...))
|
||||||
|
|
||||||
|
inr, inw := io.Pipe()
|
||||||
|
defer inr.Close()
|
||||||
|
outr, outw := io.Pipe()
|
||||||
|
defer outw.Close()
|
||||||
|
var success atomic.Bool
|
||||||
|
go func() {
|
||||||
|
time.AfterFunc(2*time.Minute, cancel)
|
||||||
|
defer inw.Close()
|
||||||
|
defer outr.Close()
|
||||||
|
login := []byte("login:")
|
||||||
|
password := []byte("Password:")
|
||||||
|
s := bufio.NewScanner(outr)
|
||||||
|
s.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if i := bytes.Index(data, []byte(img.luks)); i >= 0 {
|
||||||
|
return i + len(img.luks), []byte(img.luks), nil
|
||||||
|
}
|
||||||
|
if i := bytes.Index(data, login); i >= 0 {
|
||||||
|
return i + len(login), login, nil
|
||||||
|
}
|
||||||
|
if i := bytes.Index(data, password); i >= 0 {
|
||||||
|
return i + len(password), password, nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return 0, nil, io.EOF
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
for s.Scan() {
|
||||||
|
b := s.Bytes()
|
||||||
|
if bytes.Contains(b, []byte(img.luks)) {
|
||||||
|
t.Logf("sending luks password")
|
||||||
|
if _, err := inw.Write([]byte("root\n")); err != nil {
|
||||||
|
t.Logf("failed to write luks password: %v", err)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Contains(b, login) {
|
||||||
|
t.Logf("sending login")
|
||||||
|
if _, err := inw.Write([]byte("root\n")); err != nil {
|
||||||
|
t.Logf("failed to write login: %v", err)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Contains(b, password) {
|
||||||
|
t.Logf("sending password")
|
||||||
|
if _, err := inw.Write([]byte("root\n")); err != nil {
|
||||||
|
t.Logf("failed to write password: %v", err)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
if _, err := inw.Write([]byte("poweroff\n")); err != nil {
|
||||||
|
t.Logf("failed to write poweroff: %v", err)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
success.Store(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
t.Logf("failed to scan output: %v", err)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
opts := []qemu.Option{qemu.WithStdin(inr), qemu.WithStdout(io.MultiWriter(outw, os.Stdout)), qemu.WithStderr(io.Discard), qemu.WithMemory(2048), qemu.WithCPUs(2)}
|
||||||
|
if tt.efi {
|
||||||
|
opts = append(opts, qemu.WithBios("/usr/share/ovmf/OVMF.fd"))
|
||||||
|
}
|
||||||
|
if err := qemu.Run(ctx, out, opts...); err != nil && !success.Load() {
|
||||||
|
t.Fatalf("failed to run qemu: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
RUN apk add --no-cache openssh-server && \
|
RUN apk add --no-cache openrc openssh-server && \
|
||||||
|
rc-update add sshd default && \
|
||||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
network:
|
|
||||||
version: 2
|
|
||||||
renderer: networkd
|
|
||||||
ethernets:
|
|
||||||
eth0:
|
|
||||||
dhcp4: true
|
|
||||||
nameservers:
|
|
||||||
addresses:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 8.8.4.4
|
|
@ -1,15 +1,13 @@
|
|||||||
FROM ubuntu
|
FROM ubuntu
|
||||||
|
|
||||||
# Install netplan sudo ssh-server and dns utils
|
# Install some system packages
|
||||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||||
qemu-guest-agent \
|
qemu-guest-agent \
|
||||||
netplan.io \
|
ca-certificates \
|
||||||
dnsutils \
|
dnsutils \
|
||||||
sudo \
|
sudo \
|
||||||
openssh-server
|
openssh-server
|
||||||
|
|
||||||
# Setup default network config
|
|
||||||
COPY 00-netconf.yaml /etc/netplan/
|
|
||||||
# Add a utility script to resize serial terminal
|
# Add a utility script to resize serial terminal
|
||||||
COPY resize /usr/local/bin/
|
COPY resize /usr/local/bin/
|
||||||
|
|
||||||
@ -19,14 +17,17 @@ ARG PASSWORD=d2vm
|
|||||||
ARG SSH_KEY=https://github.com/${USER}.keys
|
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||||
|
|
||||||
# Setup user environment
|
# Setup user environment
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||||
bash-completion \
|
bash-completion \
|
||||||
curl \
|
curl \
|
||||||
zsh \
|
zsh \
|
||||||
git \
|
git \
|
||||||
vim \
|
vim \
|
||||||
tmux \
|
tmux \
|
||||||
htop
|
htop \
|
||||||
|
lsb-core \
|
||||||
|
cloud-init \
|
||||||
|
cloud-guest-utils
|
||||||
|
|
||||||
# Create user with sudo privileged and passwordless sudo
|
# Create user with sudo privileged and passwordless sudo
|
||||||
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
||||||
@ -45,4 +46,5 @@ USER ${USER}
|
|||||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||||
# Setup tmux environment
|
# Setup tmux environment
|
||||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||||
|
# Setup auto login serial console
|
||||||
|
RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --keep-baud 115200,38400,9600 \%I \$TERM|" /usr/lib/systemd/system/serial-getty@.service
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
# d2vm full example
|
# ZSH Workstation example
|
||||||
|
|
||||||
This example demonstrate the setup of a ZSH workstation.
|
This example demonstrate the setup of a ZSH workstation with *cloud-init* support.
|
||||||
|
|
||||||
*Dockerfile*
|
*Dockerfile*
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM ubuntu
|
FROM ubuntu
|
||||||
|
|
||||||
# Install netplan sudo ssh-server and dns utils
|
# Install some system packages
|
||||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||||
qemu-guest-agent \
|
qemu-guest-agent \
|
||||||
netplan.io \
|
ca-certificates \
|
||||||
dnsutils \
|
dnsutils \
|
||||||
sudo \
|
sudo \
|
||||||
openssh-server
|
openssh-server
|
||||||
|
|
||||||
# Setup default network config
|
|
||||||
COPY 00-netconf.yaml /etc/netplan/
|
|
||||||
# Add a utility script to resize serial terminal
|
# Add a utility script to resize serial terminal
|
||||||
COPY resize /usr/local/bin/
|
COPY resize /usr/local/bin/
|
||||||
|
|
||||||
@ -25,13 +23,17 @@ ARG PASSWORD=d2vm
|
|||||||
ARG SSH_KEY=https://github.com/${USER}.keys
|
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||||
|
|
||||||
# Setup user environment
|
# Setup user environment
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||||
|
bash-completion \
|
||||||
curl \
|
curl \
|
||||||
zsh \
|
zsh \
|
||||||
git \
|
git \
|
||||||
vim \
|
vim \
|
||||||
tmux \
|
tmux \
|
||||||
htop
|
htop \
|
||||||
|
lsb-core \
|
||||||
|
cloud-init \
|
||||||
|
cloud-guest-utils
|
||||||
|
|
||||||
# Create user with sudo privileged and passwordless sudo
|
# Create user with sudo privileged and passwordless sudo
|
||||||
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
|
||||||
@ -50,23 +52,13 @@ USER ${USER}
|
|||||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||||
# Setup tmux environment
|
# Setup tmux environment
|
||||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||||
```
|
# Setup auto login serial console
|
||||||
|
RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --keep-baud 115200,38400,9600 \%I \$TERM|" /usr/lib/systemd/system/serial-getty@.service
|
||||||
*00-netconf.yaml*
|
|
||||||
```yaml
|
|
||||||
network:
|
|
||||||
version: 2
|
|
||||||
renderer: networkd
|
|
||||||
ethernets:
|
|
||||||
eth0:
|
|
||||||
dhcp4: true
|
|
||||||
nameservers:
|
|
||||||
addresses:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 8.8.4.4
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is no need to configure the network as **d2vm** will generate a *netplan* configuration that use DHCP.
|
||||||
|
|
||||||
**Build**
|
**Build**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -74,22 +66,29 @@ USER=mygithubuser
|
|||||||
PASSWORD=mysecurepasswordthatIwillneverusebecauseIuseMostlySSHkeys
|
PASSWORD=mysecurepasswordthatIwillneverusebecauseIuseMostlySSHkeys
|
||||||
OUTPUT=workstation.qcow2
|
OUTPUT=workstation.qcow2
|
||||||
|
|
||||||
d2vm build -o $OUTPUT --force --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys .
|
d2vm build -o $OUTPUT --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys --force -v .
|
||||||
```
|
```
|
||||||
|
|
||||||
Run it using *libvirt's virt-install*:
|
Run it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
virt-install --name workstation --disk $OUTPUT --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'
|
d2vm run qemu --mem 4096 --cpus 4 $IMAGE
|
||||||
```
|
```
|
||||||
|
... you should be automatically logged in with a **oh-my-zsh** shell
|
||||||
|
|
||||||
From an other terminal you should be able to find the VM ip address using:
|
You should be able to find the ip address inside the VM using:
|
||||||
```bash
|
```bash
|
||||||
virsh domifaddr --domain workstation
|
hostname -I
|
||||||
|
# or
|
||||||
|
ip a show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1
|
||||||
```
|
```
|
||||||
|
|
||||||
And connect using ssh...
|
And connect using ssh...
|
||||||
|
|
||||||
|
In order to quit the terminal you need to shut down the VM with the `poweroff` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo poweroff
|
||||||
|
```
|
||||||
|
|
||||||
*I hope you will find it useful and that you will have fun...*
|
*I hope you will find it useful and that you will have fun...*
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu
|
FROM ubuntu
|
||||||
|
|
||||||
RUN apt update && apt install -y openssh-server && \
|
RUN apt update && apt install -y openssh-server && \
|
||||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||||
|
58
fs.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BootFS string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BootFSExt4 BootFS = "ext4"
|
||||||
|
BootFSFat32 BootFS = "fat32"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f BootFS) String() string {
|
||||||
|
return string(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BootFS) IsExt() bool {
|
||||||
|
return f == BootFSExt4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BootFS) IsFat() bool {
|
||||||
|
return f == BootFSFat32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BootFS) IsSupported() bool {
|
||||||
|
return f.IsExt() || f.IsFat()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BootFS) Validate() error {
|
||||||
|
if !f.IsSupported() {
|
||||||
|
fmt.Errorf("invalid boot filesystem: %s valid filesystems are: fat32, ext4", f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BootFS) linux() string {
|
||||||
|
switch f {
|
||||||
|
case BootFSFat32:
|
||||||
|
return "vfat"
|
||||||
|
default:
|
||||||
|
return "ext4"
|
||||||
|
}
|
||||||
|
}
|
70
go.mod
@ -1,19 +1,69 @@
|
|||||||
module go.linka.cloud/d2vm
|
module go.linka.cloud/d2vm
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||||
|
github.com/dustin/go-humanize v1.0.0
|
||||||
|
github.com/fatih/color v1.13.0
|
||||||
|
github.com/google/go-containerregistry v0.14.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/hetznercloud/hcloud-go v1.50.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/pkg/sftp v1.10.1
|
||||||
go.uber.org/multierr v1.8.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/svenwiltink/sparsecat v1.0.0
|
||||||
|
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
|
||||||
|
go.uber.org/multierr v1.11.0
|
||||||
|
golang.org/x/crypto v0.11.0
|
||||||
|
golang.org/x/sys v0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/creack/pty v1.1.15 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/docker/cli v23.0.4+incompatible // indirect
|
||||||
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
|
github.com/docker/docker v23.0.4+incompatible // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.5 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.10.1 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||||
|
golang.org/x/net v0.12.0 // indirect
|
||||||
|
golang.org/x/sync v0.2.0 // indirect
|
||||||
|
golang.org/x/text v0.11.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace (
|
||||||
|
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.11.1
|
||||||
|
golang.org/x/crypto => golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b
|
||||||
|
google.golang.org/protobuf => google.golang.org/protobuf v1.29.1
|
||||||
)
|
)
|
||||||
|
208
go.sum
@ -1,37 +1,205 @@
|
|||||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 h1:t8KYCwSKsOEZBFELI4Pn/phbp38iJ1RRAkDFNin1aak=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
|
||||||
|
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docker/cli v23.0.4+incompatible h1:xClB7PsiATttDHj8ce5qvJcikiApNy7teRR1XkoBZGs=
|
||||||
|
github.com/docker/cli v23.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||||
|
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
|
||||||
|
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||||
|
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw=
|
||||||
|
github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/hetznercloud/hcloud-go v1.50.0 h1:vS9tJvmSRwgDpMLmPnThGN87Rz8OMP3D4M3rWm8QHEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/hetznercloud/hcloud-go v1.50.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
|
||||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||||
|
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
|
||||||
|
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
|
||||||
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
|
||||||
|
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
|
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||||
|
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||||
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||||
|
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
github.com/svenwiltink/sparsecat v1.0.0 h1:SBDEIImxhD//8MskqodFR9VcixWKkZAPAW35nmA4vcw=
|
||||||
|
github.com/svenwiltink/sparsecat v1.0.0/go.mod h1:TdtvJbeTZcd+3cMQpttW6MJl/iPGZT0GHmckep0hoxU=
|
||||||
|
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||||
|
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
|
||||||
|
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
||||||
|
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b h1:LccDAOuhRuyJqirU7I34UEEoYvuWWJ2jbs2Hyuc2aYU=
|
||||||
|
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b/go.mod h1:P/4MMfexPBBlmMpefLne7phPoNice1vhX65ZqOfzGII=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=
|
||||||
|
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||||
|
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||||
|
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||||
|
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
|
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||||
|
76
grub.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grub struct {
|
||||||
|
*grubCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grub) Validate(fs BootFS) error {
|
||||||
|
switch fs {
|
||||||
|
case BootFSFat32:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("grub only supports fat32 boot filesystem due to grub-efi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grub) Setup(ctx context.Context, dev, root string, cmdline string) error {
|
||||||
|
logrus.Infof("setting up grub bootloader")
|
||||||
|
clean, err := g.prepare(ctx, dev, root, cmdline)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer clean()
|
||||||
|
if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := g.mkconfig(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type grubProvider struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
|
||||||
|
if arch != "x86_64" {
|
||||||
|
return nil, fmt.Errorf("grub is only supported for amd64")
|
||||||
|
}
|
||||||
|
if r.ID == ReleaseCentOS {
|
||||||
|
return nil, fmt.Errorf("grub (efi) is not supported for CentOS, use grub-bios instead")
|
||||||
|
}
|
||||||
|
return grub{grubCommon: newGrubCommon(c, r)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubProvider) Name() string {
|
||||||
|
return "grub"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterBootloaderProvider(grubProvider{})
|
||||||
|
}
|
65
grub_bios.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grubBios struct {
|
||||||
|
*grubCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubBios) Validate(_ BootFS) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubBios) Setup(ctx context.Context, dev, root string, cmdline string) error {
|
||||||
|
logrus.Infof("setting up grub bootloader")
|
||||||
|
clean, err := g.prepare(ctx, dev, root, cmdline)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer clean()
|
||||||
|
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := g.mkconfig(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type grubBiosProvider struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubBiosProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
|
||||||
|
if arch != "x86_64" {
|
||||||
|
return nil, fmt.Errorf("grub-bios is only supported for amd64")
|
||||||
|
}
|
||||||
|
return grubBios{grubCommon: newGrubCommon(c, r)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubBiosProvider) Name() string {
|
||||||
|
return "grub-bios"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterBootloaderProvider(grubBiosProvider{})
|
||||||
|
}
|
102
grub_common.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const grubCfg = `GRUB_DEFAULT=0
|
||||||
|
GRUB_HIDDEN_TIMEOUT=0
|
||||||
|
GRUB_HIDDEN_TIMEOUT_QUIET=true
|
||||||
|
GRUB_TIMEOUT=0
|
||||||
|
GRUB_CMDLINE_LINUX_DEFAULT="%s"
|
||||||
|
GRUB_CMDLINE_LINUX=""
|
||||||
|
GRUB_TERMINAL=console
|
||||||
|
`
|
||||||
|
|
||||||
|
type grubCommon struct {
|
||||||
|
name string
|
||||||
|
c Config
|
||||||
|
r OSRelease
|
||||||
|
root string
|
||||||
|
dev string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrubCommon(c Config, r OSRelease) *grubCommon {
|
||||||
|
name := "grub"
|
||||||
|
if r.ID == "centos" {
|
||||||
|
name = "grub2"
|
||||||
|
}
|
||||||
|
return &grubCommon{
|
||||||
|
name: name,
|
||||||
|
c: c,
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (clean func(), err error) {
|
||||||
|
g.dev = dev
|
||||||
|
g.root = root
|
||||||
|
if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mounts := []string{"dev", "proc", "sys"}
|
||||||
|
var unmounts []string
|
||||||
|
clean = func() {
|
||||||
|
for _, v := range unmounts {
|
||||||
|
if err := exec.Run(ctx, "umount", filepath.Join(root, v)); err != nil {
|
||||||
|
logrus.Errorf("failed to unmount /%s: %s", v, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
clean()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for _, v := range mounts {
|
||||||
|
if err = exec.Run(ctx, "mount", "-o", "bind", "/"+v, filepath.Join(root, v)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unmounts = append(unmounts, v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grubCommon) install(ctx context.Context, args ...string) error {
|
||||||
|
if g.dev == "" || g.root == "" {
|
||||||
|
return fmt.Errorf("grubCommon not prepared")
|
||||||
|
}
|
||||||
|
args = append([]string{g.root, g.name + "-install"}, args...)
|
||||||
|
return exec.Run(ctx, "chroot", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grubCommon) mkconfig(ctx context.Context) error {
|
||||||
|
if g.dev == "" || g.root == "" {
|
||||||
|
return fmt.Errorf("grubCommon not prepared")
|
||||||
|
}
|
||||||
|
return exec.Run(ctx, "chroot", g.root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg")
|
||||||
|
}
|
71
grub_efi.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grubEFI struct {
|
||||||
|
*grubCommon
|
||||||
|
arch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubEFI) Validate(fs BootFS) error {
|
||||||
|
switch fs {
|
||||||
|
case BootFSFat32:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("grub-efi only supports fat32 boot filesystem")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) error {
|
||||||
|
logrus.Infof("setting up grub-efi bootloader")
|
||||||
|
clean, err := g.prepare(ctx, dev, root, cmdline)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer clean()
|
||||||
|
if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := g.mkconfig(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type grubEFIProvider struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubEFIProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
|
||||||
|
if r.ID == ReleaseCentOS {
|
||||||
|
return nil, fmt.Errorf("grub-efi is not supported for CentOS, use grub-bios instead")
|
||||||
|
}
|
||||||
|
return grubEFI{grubCommon: newGrubCommon(c, r), arch: arch}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g grubEFIProvider) Name() string {
|
||||||
|
return "grub-efi"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterBootloaderProvider(grubEFIProvider{})
|
||||||
|
}
|
@ -16,11 +16,8 @@ package d2vm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"strconv"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -34,6 +31,7 @@ const (
|
|||||||
ReleaseAlpine Release = "alpine"
|
ReleaseAlpine Release = "alpine"
|
||||||
ReleaseCentOS Release = "centos"
|
ReleaseCentOS Release = "centos"
|
||||||
ReleaseRHEL Release = "rhel"
|
ReleaseRHEL Release = "rhel"
|
||||||
|
ReleaseKali Release = "kali"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Release string
|
type Release string
|
||||||
@ -44,6 +42,8 @@ func (r Release) Supported() bool {
|
|||||||
return true
|
return true
|
||||||
case ReleaseDebian:
|
case ReleaseDebian:
|
||||||
return true
|
return true
|
||||||
|
case ReleaseKali:
|
||||||
|
return true
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
return true
|
return true
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS:
|
||||||
@ -63,6 +63,31 @@ type OSRelease struct {
|
|||||||
VersionCodeName string
|
VersionCodeName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r OSRelease) SupportsLUKS() bool {
|
||||||
|
switch r.ID {
|
||||||
|
case ReleaseUbuntu:
|
||||||
|
return r.VersionID >= "20.04"
|
||||||
|
case ReleaseDebian:
|
||||||
|
v, err := strconv.Atoi(r.VersionID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("%s: failed to parse version id: %v", r.Version, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v >= 10
|
||||||
|
case ReleaseKali:
|
||||||
|
// TODO: check version
|
||||||
|
return true
|
||||||
|
case ReleaseCentOS:
|
||||||
|
return true
|
||||||
|
case ReleaseAlpine:
|
||||||
|
return true
|
||||||
|
case ReleaseRHEL:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ParseOSRelease(s string) (OSRelease, error) {
|
func ParseOSRelease(s string) (OSRelease, error) {
|
||||||
env, err := godotenv.Parse(strings.NewReader(s))
|
env, err := godotenv.Parse(strings.NewReader(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -78,40 +103,8 @@ func ParseOSRelease(s string) (OSRelease, error) {
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func FetchDockerImageOSRelease(ctx context.Context, img string) (OSRelease, error) {
|
||||||
osReleaseDockerfile = `
|
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", "--entrypoint", "cat", img, "/etc/os-release")
|
||||||
FROM {{ . }}
|
|
||||||
|
|
||||||
ENTRYPOINT [""]
|
|
||||||
|
|
||||||
CMD ["/bin/cat", "/etc/os-release"]
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
osReleaseDockerfileTemplate = template.Must(template.New("osrelease.Dockerfile").Parse(osReleaseDockerfile))
|
|
||||||
)
|
|
||||||
|
|
||||||
func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) {
|
|
||||||
d := filepath.Join(tmpPath, "osrelase.Dockerfile")
|
|
||||||
f, err := os.Create(d)
|
|
||||||
if err != nil {
|
|
||||||
return OSRelease{}, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
if err := osReleaseDockerfileTemplate.Execute(f, img); err != nil {
|
|
||||||
return OSRelease{}, err
|
|
||||||
}
|
|
||||||
imgTag := fmt.Sprintf("os-release-%s", img)
|
|
||||||
if err := docker.Cmd(ctx, "image", "build", "-t", imgTag, "-f", d, tmpPath); err != nil {
|
|
||||||
return OSRelease{}, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := docker.Cmd(ctx, "image", "rm", imgTag); err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to cleanup OSRelease Docker Image")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", imgTag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return OSRelease{}, err
|
return OSRelease{}, err
|
||||||
}
|
}
|
||||||
|
51
pgp.pub
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGLCuWcBEADzbuC8tyB0zPmReCu0Jwvc9tJqErtaYXxizM2XGiYc6iYOheFb
|
||||||
|
FnSFwlWK4TmtZ0XxIMMMJpIMVy1eEdbv3SBqHYXDWu+FzbEF32zfh/Sp4jzTbAZy
|
||||||
|
eKdXdcKYShhBNBnvfdQBsKG8J5PJi37w9yX9IVfviPymz1j42w7kvPMt6KM0HC3q
|
||||||
|
tJzwYLt6yizpY1DkT1lhypK2cgWrbBBTzxiUVHAK1Zxxr+MkSFowW5MHV0nfVWL2
|
||||||
|
5RU9mJny98YYvUJ7cQ1WaBjDQ9LSwMi+BVfZ87ABsc6NOyxPyEq7g/qwXdWL9QVq
|
||||||
|
bVFKvj2AaYeVJn17LYG3Ao8FfAt2tUN3FasrvmXnXumkqErwrCkGG5LFuRE11X/2
|
||||||
|
ntSf2Ra+dp81XHmugGHKdqdridQZPFTHeMQgk5Nyo0nNaQ0dO8w0zvLEqqQlEe4O
|
||||||
|
lTEK6MRPN2Pq9Rl2dC4YHU6ctnBEIwf98ysK2oIuHEnt8YZALg1jB2a1ANYOy25Z
|
||||||
|
UHOr2FlhCD6kdVpwGp99NH0lhKRcGl9BFDakYGWkjCqZT3UAZa+qKIp+wfNn75kI
|
||||||
|
1WjsIg0JFeH57RydlZf0eCelHY90rUKLB0kAlt/mWjoDyFnfrVHVfDh4yb/Gnwts
|
||||||
|
0cFWmink7fB0OLvzQvNGYPDkGG9tyjmepFm3OdD+wi2x3fPhPnB/o+sXXQARAQAB
|
||||||
|
tCJsaW5rYS1jbG91ZCA8c2VjdXJpdHlAbGlua2EtY2xvdWQ+iQJOBBMBCgA4FiEE
|
||||||
|
dRYaVRQmvhJpUDzmgEjKN+QesW0FAmLCuWcCGwMFCwkIBwIGFQoJCAsCBBYCAwEC
|
||||||
|
HgECF4AACgkQgEjKN+QesW0feQ/9H3HOjEv45em9JrdQu9UqjIG7VJG8En0fy2K2
|
||||||
|
oSQ65rOCRk1oxet2hay/jIkNfXwhSQ7pAOzs69JnDrXLYS9gZ3gSHRoWXSRe1way
|
||||||
|
mPIorq0hxaBJ6iJybmgOj4gqFmK+ObwcGUZq8y9hdD1UkQG9zM3+jPOh9Bd9t8ld
|
||||||
|
S8H4Sew1ZC2vVqh281BxvmJM9w94EuVw+4gJ72Rg74W2TOhK0qEvblHU7UbLI3s5
|
||||||
|
w+WvoLHQw3V6DmhuTsFWXWY1WXtlsYa5dE3QLWtXqCzt+yL1lw1opxgnP5BTBwjP
|
||||||
|
v5nZCWw6RC1EJxd2ac5IK8bXLcM0BtmHbVqA7cbeU4jc5TBj0Lu5o1AfUUCbyRMg
|
||||||
|
i7LDVn9ivCD3mymyGCtNsdj+oad9MwKJMlHNwzWNf2yE8GjxO09RYx/AzlubozCG
|
||||||
|
qlJvsEQUvUftGDW1adSdT+QRfIIS9pg2nZbMe+U0udrjpV1OGZDPgO7UNxi9u2Kl
|
||||||
|
JrVZ460K2psJEOBOaHg6Baj3HU1Ac6+UCigXbOx7WD7o3Rj8eNf1bIX/MXPfbBkv
|
||||||
|
Hh4dUchPuo1ImvU59w/jseuyXdiijCo5b5qeK2227XvXHIy3138x6gzs6JL5TXRp
|
||||||
|
F+D87+2WDu3yKjch0Sk0t+dKvuwsul/17wzDvMBIBMW96hRJhDVbUfaMPahf6Cfp
|
||||||
|
Gk3+t5W5Ag0EYsK5ZwEQAMJsn7eADPF9GshQRkzCcSxkeCh1OXWgPTOVEkBwKTeH
|
||||||
|
TXTEV4seANFHeUrTru3U/uuCsWi8eiwe5HTJKtPANud1iaUuMn7AyU0NteC8Bk9U
|
||||||
|
duwQXt02nke5jruNYOBm4j5yQYIBfa75ziDLUz7+NeAXIc3DhRM9gtE4N+5L95p+
|
||||||
|
bPPI077TldUkSLZM2kVIeWiAmZ1zsTg6SDW8wFMBoFfOtkEffZco4gzHlj8vPAc6
|
||||||
|
jkbgwrH7RZsWBcz2t7l/1AycDNPTElgFxnLxmBG01bNQuXTviNMZ0trNCtk2RdkS
|
||||||
|
iSLhIc3hMmaxnqy2mDzCVq/DCERo54iadXTJew9Y5jh2cO6V/rTJSnwaIh9AHMor
|
||||||
|
VO/kWPOzC/XMj1kL27DMwbByzm3619p0FgLPYqMdiWkmGhjVr0kcM+sBcepMliRs
|
||||||
|
eQy6q47IFm90XyUL9IXMtLWeleQ5/zHjD1CsKc/bSB5lzxLsv/IckYq8c9bbbPA0
|
||||||
|
bCGosQNQQMZTR/9sJ5VmYOWzDimqvX8l12GvYiEQQFO/lTypYjtE3XxjtXGRIA+4
|
||||||
|
i1t20Lmy2Scl1WV+LMbhk0i+sEBOzuD5jctQWqX68KYYUqWbiqULsrdHtgodITDG
|
||||||
|
CrlVvwB9BACb2JVDjbUhY3VDl3yHopmGM0kMZGLUvRaJOnCh2Dc+B/cCt/iiO6UB
|
||||||
|
ABEBAAGJAjYEGAEKACAWIQR1FhpVFCa+EmlQPOaASMo35B6xbQUCYsK5ZwIbDAAK
|
||||||
|
CRCASMo35B6xbRuWEACFH9cR36izop9hOu7oEnwLABGC6U4mipTvgKD4wu1SS0U4
|
||||||
|
NRPzkTm7FGxXy2QPbDOj1/G7dvHc7fGzQimofGRIW/4/GVWRQi2pQJwKTP1KlRid
|
||||||
|
e9oFG1+MTc6o1ZBkhz1GQbMGxeu9Na0c0DzXGMrsP6G75WSUEX+5srXuJtrxRrBy
|
||||||
|
E3BICzn5YyWTT2cLgN3AucalL04TIqGiocvi3X/n03CXe2M2mbJFo1y1bl6M/tmR
|
||||||
|
0fktpVlCDRs6EphMXHsKSTtO6pKPN6M0Lg50vS512xNGyc/tL2aJw/snO0vMDHSU
|
||||||
|
JDebH4LjV1RmTRnUJxERDCfe8Jh1bsIa4u8DcVmoLyjTMXJrPNUycYIkyfp9mwAX
|
||||||
|
LjBY2mW6Qyfp3IVdQUS1N8okIk1TiHIZC7DXdXuH7WnfEDDTV9fY91jY835y8stj
|
||||||
|
lpX0UGjm+npt2Vyth9kIQtgUnnBl76PdjAmCHztEaL1SXiQh6h2DhdkfeVX4GUrF
|
||||||
|
BE02K7Qy6ERZo7274WLlJOiW5EyyOKVHYrSzRLajsh8xHus270FhfcKRQG+LO8f+
|
||||||
|
2ecvNRuNZcMiwZDUwSzUkabXk8C9F/3EOcUnHoznh/g//Z17/ZktIuJy4DLLkYIk
|
||||||
|
jtPL12rsffEIbEo7Ok/ntyVf5rgitiHu5xSTzjG6cj0olm46rsJ0h3hij58MOw==
|
||||||
|
=3bbH
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
28
pkg/docker/check_term_unix.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isInteractive() bool {
|
||||||
|
_, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
|
||||||
|
return err == nil
|
||||||
|
}
|
36
pkg/docker/check_term_windows.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isInteractive() bool {
|
||||||
|
handle := windows.Handle(os.Stdout.Fd())
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
if err := windows.SetConsoleMode(handle, mode); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
@ -15,13 +15,27 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/exec"
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func dockerSocket() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "//var/run/docker.sock"
|
||||||
|
}
|
||||||
|
return "/var/run/docker.sock"
|
||||||
|
}
|
||||||
|
|
||||||
func FormatImgName(name string) string {
|
func FormatImgName(name string) string {
|
||||||
s := strings.Replace(name, ":", "-", -1)
|
s := strings.Replace(name, ":", "-", -1)
|
||||||
s = strings.Replace(s, "/", "_", -1)
|
s = strings.Replace(s, "/", "_", -1)
|
||||||
@ -35,3 +49,122 @@ func Cmd(ctx context.Context, args ...string) error {
|
|||||||
func CmdOut(ctx context.Context, args ...string) (string, string, error) {
|
func CmdOut(ctx context.Context, args ...string) (string, string, error) {
|
||||||
return exec.RunOut(ctx, "docker", args...)
|
return exec.RunOut(ctx, "docker", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Build(ctx context.Context, pull bool, tag, dockerfile, dir, platform string, buildArgs ...string) error {
|
||||||
|
if dockerfile == "" {
|
||||||
|
dockerfile = filepath.Join(dir, "Dockerfile")
|
||||||
|
}
|
||||||
|
args := []string{"image", "build", "-t", tag, "-f", dockerfile, "--platform", platform}
|
||||||
|
if pull {
|
||||||
|
args = append(args, "--pull")
|
||||||
|
}
|
||||||
|
for _, v := range buildArgs {
|
||||||
|
args = append(args, "--build-arg", v)
|
||||||
|
}
|
||||||
|
args = append(args, dir)
|
||||||
|
return Cmd(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Tag(ctx context.Context, img string, tags ...string) error {
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return fmt.Errorf("no tags specified")
|
||||||
|
}
|
||||||
|
args := []string{"image", "tag"}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if err := Cmd(ctx, append(args, img, tag)...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Remove(ctx context.Context, tag string) error {
|
||||||
|
return Cmd(ctx, "image", "rm", tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageList(ctx context.Context, tag string) ([]string, error) {
|
||||||
|
o, _, err := CmdOut(ctx, "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := bufio.NewScanner(strings.NewReader(o))
|
||||||
|
var imgs []string
|
||||||
|
for s.Scan() {
|
||||||
|
imgs = append(imgs, s.Text())
|
||||||
|
}
|
||||||
|
return imgs, s.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageSave(ctx context.Context, tag, file string) error {
|
||||||
|
return Cmd(ctx, "image", "save", "-o", file, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pull(ctx context.Context, platform, tag string) error {
|
||||||
|
return Cmd(ctx, "image", "pull", "--platform", platform, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Push(ctx context.Context, tag string) error {
|
||||||
|
return Cmd(ctx, "image", "push", tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunInteractiveAndRemove(ctx context.Context, args ...string) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunAndRemove(ctx context.Context, args ...string) error {
|
||||||
|
logrus.Tracef("running 'docker run --rm %s'", strings.Join(args, " "))
|
||||||
|
return Cmd(ctx, append([]string{"run", "--rm"}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...string) error {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if in == "" {
|
||||||
|
in = pwd
|
||||||
|
}
|
||||||
|
if out == "" {
|
||||||
|
out = pwd
|
||||||
|
}
|
||||||
|
if image == "" {
|
||||||
|
image = "linkacloud/d2vm"
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
a := []string{"run", "--rm"}
|
||||||
|
|
||||||
|
interactive := isInteractive()
|
||||||
|
|
||||||
|
if interactive {
|
||||||
|
a = append(a, "-i", "-t")
|
||||||
|
}
|
||||||
|
a = append(a,
|
||||||
|
"--privileged",
|
||||||
|
"-e",
|
||||||
|
// yes... it is kind of a dirty hack
|
||||||
|
fmt.Sprintf("SUDO_UID=%d", os.Getuid()),
|
||||||
|
"-v",
|
||||||
|
fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()),
|
||||||
|
"-v",
|
||||||
|
fmt.Sprintf("%s:/in", in),
|
||||||
|
"-v",
|
||||||
|
fmt.Sprintf("%s:/out", out),
|
||||||
|
"-w",
|
||||||
|
"/d2vm",
|
||||||
|
fmt.Sprintf("%s:%s", image, version),
|
||||||
|
cmd,
|
||||||
|
)
|
||||||
|
c := exec.CommandContext(ctx, "docker", append(a, args...)...)
|
||||||
|
if interactive {
|
||||||
|
c.Stdin = os.Stdin
|
||||||
|
}
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
c.Stderr = os.Stderr
|
||||||
|
return c.Run()
|
||||||
|
}
|
||||||
|
@ -18,21 +18,35 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Run = RunNoOut
|
Run = RunNoOut
|
||||||
|
|
||||||
CommandContext = exec.CommandContext
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunStdout(ctx context.Context, c string, args ...string) error {
|
func SetDebug(debug bool) {
|
||||||
|
if debug {
|
||||||
|
Run = RunDebug
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
} else {
|
||||||
|
Run = RunNoOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommandContext(ctx context.Context, c string, args ...string) *exec.Cmd {
|
||||||
|
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
|
||||||
|
return exec.CommandContext(ctx, c, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunDebug(ctx context.Context, c string, args ...string) error {
|
||||||
|
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
|
||||||
cmd := exec.CommandContext(ctx, c, args...)
|
cmd := exec.CommandContext(ctx, c, args...)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
148
pkg/qemu/config.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(c *config)
|
||||||
|
|
||||||
|
type Disk struct {
|
||||||
|
Path string
|
||||||
|
Size int
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishedPort struct {
|
||||||
|
Guest uint16
|
||||||
|
Host uint16
|
||||||
|
Protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
// config contains the config for Qemu
|
||||||
|
type config struct {
|
||||||
|
path string
|
||||||
|
uuid uuid.UUID
|
||||||
|
gui bool
|
||||||
|
disks []Disk
|
||||||
|
networking string
|
||||||
|
arch string
|
||||||
|
cpus uint
|
||||||
|
memory uint
|
||||||
|
bios string
|
||||||
|
accel string
|
||||||
|
detached bool
|
||||||
|
qemuBinPath string
|
||||||
|
qemuImgPath string
|
||||||
|
publishedPorts []PublishedPort
|
||||||
|
netdevConfig string
|
||||||
|
|
||||||
|
stdin io.Reader
|
||||||
|
stdout io.Writer
|
||||||
|
stderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithGUI() Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.gui = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDisks(disks ...Disk) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.disks = disks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNetworking(networking string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.networking = networking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithArch(arch string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.arch = arch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCPUs(cpus uint) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.cpus = cpus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMemory(memory uint) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.memory = memory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBios(bios string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.bios = bios
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAccel(accel string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.accel = accel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDetached() Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.detached = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQemuBinPath(path string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.qemuBinPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQemuImgPath(path string) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.qemuImgPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPublishedPorts(ports ...PublishedPort) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.publishedPorts = ports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStdin(r io.Reader) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.stdin = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStdout(w io.Writer) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.stdout = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStderr(w io.Writer) Option {
|
||||||
|
return func(c *config) {
|
||||||
|
c.stderr = w
|
||||||
|
}
|
||||||
|
}
|
365
pkg/qemu/qemu.go
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NetworkingNone = "none"
|
||||||
|
NetworkingUser = "user"
|
||||||
|
NetworkingTap = "tap"
|
||||||
|
NetworkingBridge = "bridge"
|
||||||
|
NetworkingDefault = NetworkingUser
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultArch string
|
||||||
|
defaultAccel string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "arm64":
|
||||||
|
defaultArch = "aarch64"
|
||||||
|
case "amd64":
|
||||||
|
defaultArch = "x86_64"
|
||||||
|
case "s390x":
|
||||||
|
defaultArch = "s390x"
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case runtime.GOARCH == "s390x":
|
||||||
|
defaultAccel = "kvm"
|
||||||
|
case haveKVM():
|
||||||
|
defaultAccel = "kvm:tcg"
|
||||||
|
case runtime.GOOS == "darwin":
|
||||||
|
defaultAccel = "hvf:tcg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ctx context.Context, path string, opts ...Option) error {
|
||||||
|
config := &config{}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.path = path
|
||||||
|
|
||||||
|
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
|
||||||
|
config.uuid = uuid.New()
|
||||||
|
// These envvars override the corresponding command line
|
||||||
|
// options. So this must remain after the `flags.Parse` above.
|
||||||
|
// accel = GetStringValue("LINUXKIT_QEMU_ACCEL", accel, "")
|
||||||
|
|
||||||
|
if config.arch == "" {
|
||||||
|
config.arch = defaultArch
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.accel == "" {
|
||||||
|
config.accel = defaultAccel
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.cpus == 0 {
|
||||||
|
config.cpus = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.memory == 0 {
|
||||||
|
config.memory = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range config.disks {
|
||||||
|
id := ""
|
||||||
|
if i != 0 {
|
||||||
|
id = strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
if d.Size != 0 && d.Format == "" {
|
||||||
|
d.Format = "qcow2"
|
||||||
|
}
|
||||||
|
if d.Size != 0 && d.Path == "" {
|
||||||
|
d.Path = "disk" + id + ".img"
|
||||||
|
}
|
||||||
|
if d.Path == "" {
|
||||||
|
return fmt.Errorf("disk specified with no size or name")
|
||||||
|
}
|
||||||
|
config.disks[i] = d
|
||||||
|
}
|
||||||
|
|
||||||
|
config.disks = append([]Disk{{Path: config.path}}, config.disks...)
|
||||||
|
|
||||||
|
if config.networking == "" || config.networking == "default" {
|
||||||
|
dflt := NetworkingDefault
|
||||||
|
config.networking = dflt
|
||||||
|
}
|
||||||
|
netMode := strings.SplitN(config.networking, ",", 2)
|
||||||
|
|
||||||
|
switch netMode[0] {
|
||||||
|
case NetworkingUser:
|
||||||
|
config.netdevConfig = "user,id=t0"
|
||||||
|
case NetworkingTap:
|
||||||
|
if len(netMode) != 2 {
|
||||||
|
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingTap)
|
||||||
|
}
|
||||||
|
if len(config.publishedPorts) != 0 {
|
||||||
|
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||||
|
}
|
||||||
|
config.netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
|
||||||
|
case NetworkingBridge:
|
||||||
|
if len(netMode) != 2 {
|
||||||
|
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingBridge)
|
||||||
|
}
|
||||||
|
if len(config.publishedPorts) != 0 {
|
||||||
|
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||||
|
}
|
||||||
|
config.netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
|
||||||
|
case NetworkingNone:
|
||||||
|
if len(config.publishedPorts) != 0 {
|
||||||
|
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||||
|
}
|
||||||
|
config.netdevConfig = ""
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Invalid networking mode: %s", netMode[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.discoverBinaries(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.runQemuLocal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *config) runQemuLocal(ctx context.Context) (err error) {
|
||||||
|
var args []string
|
||||||
|
args, err = c.buildQemuCmdline()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range c.disks {
|
||||||
|
// If disk doesn't exist then create one
|
||||||
|
if _, err := os.Stat(d.Path); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Debugf("Creating new qemu disk [%s] format %s", d.Path, d.Format)
|
||||||
|
qemuImgCmd := exec.Command(c.qemuImgPath, "create", "-f", d.Format, d.Path, fmt.Sprintf("%dM", d.Size))
|
||||||
|
log.Debugf("%v", qemuImgCmd.Args)
|
||||||
|
if err := qemuImgCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("Error creating disk [%s] format %s: %s", d.Path, d.Format, err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Using existing disk [%s] format %s", d.Path, d.Format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detached mode is only supported in a container.
|
||||||
|
if c.detached == true {
|
||||||
|
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuCmd := exec.CommandContext(ctx, c.qemuBinPath, args...)
|
||||||
|
// If verbosity is enabled print out the full path/arguments
|
||||||
|
log.Debugf("%v", qemuCmd.Args)
|
||||||
|
|
||||||
|
// If we're not using a separate window then link the execution to stdin/out
|
||||||
|
if c.gui == true {
|
||||||
|
qemuCmd.Stdin = nil
|
||||||
|
qemuCmd.Stdout = nil
|
||||||
|
qemuCmd.Stderr = nil
|
||||||
|
} else {
|
||||||
|
qemuCmd.Stdin = c.stdin
|
||||||
|
qemuCmd.Stdout = c.stdout
|
||||||
|
qemuCmd.Stderr = c.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return qemuCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *config) buildQemuCmdline() ([]string, error) {
|
||||||
|
// Iterate through the flags and build arguments
|
||||||
|
var qemuArgs []string
|
||||||
|
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", c.cpus))
|
||||||
|
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", c.memory))
|
||||||
|
qemuArgs = append(qemuArgs, "-uuid", c.uuid.String())
|
||||||
|
|
||||||
|
if c.bios != "" {
|
||||||
|
qemuArgs = append(qemuArgs, "-bios", c.bios)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to specify the vcpu type when running qemu on arm64 platform, for security reason,
|
||||||
|
// the vcpu should be "host" instead of other names such as "cortex-a53"...
|
||||||
|
if c.arch == "aarch64" && runtime.GOARCH != "arm64" {
|
||||||
|
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-cpu", "host")
|
||||||
|
}
|
||||||
|
|
||||||
|
// goArch is the GOARCH equivalent of config.Arch
|
||||||
|
var goArch string
|
||||||
|
switch c.arch {
|
||||||
|
case "s390x":
|
||||||
|
goArch = "s390x"
|
||||||
|
case "aarch64":
|
||||||
|
goArch = "arm64"
|
||||||
|
case "x86_64":
|
||||||
|
goArch = "amd64"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s is an unsupported architecture.", c.arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if goArch != runtime.GOARCH {
|
||||||
|
log.Infof("Disable acceleration as %s != %s", c.arch, runtime.GOARCH)
|
||||||
|
c.accel = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.accel != "" {
|
||||||
|
switch c.arch {
|
||||||
|
case "s390x":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", c.accel))
|
||||||
|
case "aarch64":
|
||||||
|
gic := ""
|
||||||
|
// VCPU supports less PA bits (36) than requested by the memory map (40)
|
||||||
|
highmem := "highmem=off,"
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// gic-version=host requires KVM, which implies Linux
|
||||||
|
gic = "gic_version=host,"
|
||||||
|
highmem = ""
|
||||||
|
}
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("virt,%s%saccel=%s", gic, highmem, c.accel))
|
||||||
|
default:
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", c.accel))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch c.arch {
|
||||||
|
case "s390x":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "s390-ccw-virtio")
|
||||||
|
case "aarch64":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "virt")
|
||||||
|
default:
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "q35")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rng-random does not work on macOS
|
||||||
|
// Temporarily disable it until fixed upstream.
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
rng := "rng-random,id=rng0"
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
rng = rng + ",filename=/dev/urandom"
|
||||||
|
}
|
||||||
|
if c.arch == "s390x" {
|
||||||
|
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-ccw,rng=rng0")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-pci,rng=rng0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastDisk int
|
||||||
|
for i, d := range c.disks {
|
||||||
|
index := i
|
||||||
|
if d.Format != "" {
|
||||||
|
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",format="+d.Format+",index="+strconv.Itoa(index)+",media=disk")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",index="+strconv.Itoa(index)+",media=disk")
|
||||||
|
}
|
||||||
|
lastDisk = index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CDROMs start from at least hdc
|
||||||
|
if lastDisk < 2 {
|
||||||
|
lastDisk = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.netdevConfig != "" {
|
||||||
|
mac := generateMAC()
|
||||||
|
if c.arch == "s390x" {
|
||||||
|
qemuArgs = append(qemuArgs, "-device", "virtio-net-ccw,netdev=t0,mac="+mac.String())
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-device", "virtio-net-pci,netdev=t0,mac="+mac.String())
|
||||||
|
}
|
||||||
|
forwardings, err := buildQemuForwardings(c.publishedPorts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
qemuArgs = append(qemuArgs, "-netdev", c.netdevConfig+forwardings)
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-net", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.gui != true {
|
||||||
|
qemuArgs = append(qemuArgs, "-nographic")
|
||||||
|
}
|
||||||
|
|
||||||
|
return qemuArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *config) discoverBinaries() error {
|
||||||
|
if c.qemuImgPath != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuBinPath := "qemu-system-" + c.arch
|
||||||
|
qemuImgPath := "qemu-img"
|
||||||
|
|
||||||
|
var err error
|
||||||
|
c.qemuBinPath, err = exec.LookPath(qemuBinPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.qemuImgPath, err = exec.LookPath(qemuImgPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQemuForwardings(publishedPorts []PublishedPort) (string, error) {
|
||||||
|
if len(publishedPorts) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
var forwardings string
|
||||||
|
for _, p := range publishedPorts {
|
||||||
|
hostPort := p.Host
|
||||||
|
guestPort := p.Guest
|
||||||
|
|
||||||
|
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
return forwardings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveKVM() bool {
|
||||||
|
_, err := os.Stat("/dev/kvm")
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMAC() net.HardwareAddr {
|
||||||
|
mac := make([]byte, 6)
|
||||||
|
n, err := rand.Read(mac)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Fatal("failed to generate random mac address")
|
||||||
|
}
|
||||||
|
if n != 6 {
|
||||||
|
log.WithError(err).Fatalf("generated %d bytes for random mac address", n)
|
||||||
|
}
|
||||||
|
mac[0] &^= 0x01 // Clear multicast bit
|
||||||
|
mac[0] |= 0x2 // Set locally administered bit
|
||||||
|
return mac
|
||||||
|
}
|
114
pkg/qemu_img/qemu_img.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package qemu_img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
exec2 "go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DockerImageName string
|
||||||
|
DockerImageVersion string
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImgInfo struct {
|
||||||
|
VirtualSize int `json:"virtual-size"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
ActualSize int `json:"actual-size"`
|
||||||
|
DirtyFlag bool `json:"dirty-flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(ctx context.Context, in string) (*ImgInfo, error) {
|
||||||
|
var (
|
||||||
|
o []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if path, _ := exec.LookPath("qemu-img"); path == "" {
|
||||||
|
inAbs, err := filepath.Abs(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get absolute path for %q: %v", path, err)
|
||||||
|
}
|
||||||
|
inMount := filepath.Dir(inAbs)
|
||||||
|
in := filepath.Join("/in", filepath.Base(inAbs))
|
||||||
|
o, err = exec2.CommandContext(
|
||||||
|
ctx,
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"-v",
|
||||||
|
inMount+":/in",
|
||||||
|
"--entrypoint",
|
||||||
|
"qemu-img",
|
||||||
|
fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion),
|
||||||
|
"info",
|
||||||
|
in,
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
).CombinedOutput()
|
||||||
|
} else {
|
||||||
|
o, err = exec2.CommandContext(ctx, "qemu-img", "info", in, "--output", "json").CombinedOutput()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%v: %s", err, string(o))
|
||||||
|
}
|
||||||
|
var i ImgInfo
|
||||||
|
if err := json.Unmarshal(o, &i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert(ctx context.Context, format, in, out string) error {
|
||||||
|
if path, _ := exec.LookPath("qemu-img"); path != "" {
|
||||||
|
return exec2.Run(ctx, "qemu-img", "convert", "-O", format, in, out)
|
||||||
|
}
|
||||||
|
inAbs, err := filepath.Abs(in)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path for %q: %v", in, err)
|
||||||
|
}
|
||||||
|
inMount := filepath.Dir(inAbs)
|
||||||
|
in = filepath.Join("/in", filepath.Base(inAbs))
|
||||||
|
|
||||||
|
outAbs, err := filepath.Abs(out)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path for %q: %v", out, err)
|
||||||
|
}
|
||||||
|
outMount := filepath.Dir(outAbs)
|
||||||
|
out = filepath.Join("/out", filepath.Base(outAbs))
|
||||||
|
|
||||||
|
return docker.RunAndRemove(
|
||||||
|
ctx,
|
||||||
|
"-v",
|
||||||
|
fmt.Sprintf("%s:/in", inMount),
|
||||||
|
"-v",
|
||||||
|
fmt.Sprintf("%s:/out", outMount),
|
||||||
|
"--entrypoint",
|
||||||
|
"qemu-img",
|
||||||
|
fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion),
|
||||||
|
"convert",
|
||||||
|
"-O",
|
||||||
|
format,
|
||||||
|
in,
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
}
|
75
scripts/demo
Executable file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
dir="$PWD"
|
||||||
|
scripts_dir="scripts"
|
||||||
|
if [ "$(basename $PWD)" == "$scripts_dir" ]; then
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
. ./$scripts_dir/demo-magic
|
||||||
|
|
||||||
|
TYPE_SPEED=20
|
||||||
|
EXEC_WAIT=1
|
||||||
|
DEMO_PROMPT="${CYAN}➜ ${CYAN}\W "
|
||||||
|
clear
|
||||||
|
|
||||||
|
IMAGE="./images/workstation.qcow2"
|
||||||
|
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=1
|
||||||
|
print_prompt
|
||||||
|
wait
|
||||||
|
pei "# Let's create a virtual machine from a Dockerfile"
|
||||||
|
wait
|
||||||
|
|
||||||
|
DOCKERFILE="examples/full/Dockerfile"
|
||||||
|
|
||||||
|
pei "cat $DOCKERFILE"
|
||||||
|
|
||||||
|
cp scripts/demo-magic examples/full
|
||||||
|
cp scripts/inside examples/full
|
||||||
|
cat <<EOF >> $DOCKERFILE
|
||||||
|
COPY demo-magic /home/adphi/demo-magic
|
||||||
|
COPY inside /home/adphi/inside
|
||||||
|
RUN sudo chmod +x /home/adphi/inside && echo /home/adphi/inside >> /home/adphi/.zshrc && sudo apt install -y pv
|
||||||
|
EOF
|
||||||
|
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=5
|
||||||
|
wait
|
||||||
|
PROMPT_TIMEOUT=0
|
||||||
|
|
||||||
|
EXEC_WAIT=2
|
||||||
|
pei "export PASSWORD=\"Don'tThinkTh4tIReallyUseThisPassword:)\""
|
||||||
|
pei "sudo d2vm build -s 10G -o $IMAGE --force --build-arg USER=adphi --build-arg PASSWORD=\$PASSWORD -p \$PASSWORD -v --time=relative examples/full"
|
||||||
|
|
||||||
|
rm examples/full/{demo-magic,inside}
|
||||||
|
git checkout examples/ &> /dev/null
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=1
|
||||||
|
wait
|
||||||
|
PROMPT_TIMEOUT=2
|
||||||
|
EXEC_WAIT=1
|
||||||
|
pei "# Now let's run this image"
|
||||||
|
wait
|
||||||
|
|
||||||
|
EXEC_WAIT=2
|
||||||
|
pei "sudo d2vm run qemu --cpus 4 --mem 4096 --networking default $IMAGE"
|
||||||
|
|
||||||
|
# demo continues inside the vm is soon as the boot completes
|
||||||
|
|
||||||
|
wait
|
||||||
|
|
||||||
|
EXEC_WAIT=1
|
||||||
|
|
||||||
|
pei "# Let's try to run it on a cloud provider: Hetzner..."
|
||||||
|
|
||||||
|
EXEC_WAIT=2
|
||||||
|
pei "sudo -E d2vm run hetzner --rm -v --time=relative -u adphi -i ~/.ssh/id_rsa $IMAGE"
|
||||||
|
|
||||||
|
# demo continues inside the vm is soon as the boot completes
|
||||||
|
|
||||||
|
pei "# Pretty cool rigth ? :)"
|
||||||
|
wait
|
||||||
|
|
||||||
|
cd $dir
|
220
scripts/demo-magic
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
#
|
||||||
|
# demo-magic.sh
|
||||||
|
#
|
||||||
|
# Copyright (c) 2015 Paxton Hare
|
||||||
|
#
|
||||||
|
# This script lets you script demos in bash. It runs through your demo script when you press
|
||||||
|
# ENTER. It simulates typing and runs commands.
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# the speed to "type" the text
|
||||||
|
TYPE_SPEED=20
|
||||||
|
|
||||||
|
# no wait after "p" or "pe"
|
||||||
|
NO_WAIT=false
|
||||||
|
|
||||||
|
# if > 0, will pause for this amount of seconds before automatically proceeding with any p or pe
|
||||||
|
PROMPT_TIMEOUT=0
|
||||||
|
|
||||||
|
# don't show command number unless user specifies it
|
||||||
|
SHOW_CMD_NUMS=false
|
||||||
|
|
||||||
|
EXEC_WAIT=1
|
||||||
|
|
||||||
|
|
||||||
|
# handy color vars for pretty prompts
|
||||||
|
BLACK="\033[0;30m"
|
||||||
|
BLUE="\033[0;34m"
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
GREY="\033[0;90m"
|
||||||
|
CYAN="\033[0;36m"
|
||||||
|
RED="\033[0;31m"
|
||||||
|
PURPLE="\033[0;35m"
|
||||||
|
BROWN="\033[0;33m"
|
||||||
|
WHITE="\033[1;37m"
|
||||||
|
COLOR_RESET="\033[0m"
|
||||||
|
|
||||||
|
C_NUM=0
|
||||||
|
|
||||||
|
# prompt and command color which can be overriden
|
||||||
|
DEMO_PROMPT="$ "
|
||||||
|
DEMO_CMD_COLOR=$WHITE
|
||||||
|
DEMO_COMMENT_COLOR=$GREY
|
||||||
|
|
||||||
|
##
|
||||||
|
# prints the script usage
|
||||||
|
##
|
||||||
|
function usage() {
|
||||||
|
echo -e ""
|
||||||
|
echo -e "Usage: $0 [options]"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "\tWhere options is one or more of:"
|
||||||
|
echo -e "\t-h\tPrints Help text"
|
||||||
|
echo -e "\t-d\tDebug mode. Disables simulated typing"
|
||||||
|
echo -e "\t-n\tNo wait"
|
||||||
|
echo -e "\t-w\tWaits max the given amount of seconds before proceeding with demo (e.g. '-w5')"
|
||||||
|
echo -e ""
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# wait for user to press ENTER
|
||||||
|
# if $PROMPT_TIMEOUT > 0 this will be used as the max time for proceeding automatically
|
||||||
|
##
|
||||||
|
function wait() {
|
||||||
|
if [[ "$PROMPT_TIMEOUT" == "0" ]]; then
|
||||||
|
read -rs
|
||||||
|
else
|
||||||
|
read -rst "$PROMPT_TIMEOUT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_prompt() {
|
||||||
|
# render the prompt
|
||||||
|
x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
|
||||||
|
|
||||||
|
# show command number is selected
|
||||||
|
if $SHOW_CMD_NUMS; then
|
||||||
|
printf "[$((++C_NUM))] $x"
|
||||||
|
else
|
||||||
|
printf "$x"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# print command only. Useful for when you want to pretend to run a command
|
||||||
|
#
|
||||||
|
# takes 1 param - the string command to print
|
||||||
|
#
|
||||||
|
# usage: p "ls -l"
|
||||||
|
#
|
||||||
|
##
|
||||||
|
function p() {
|
||||||
|
if [[ ${1:0:1} == "#" ]]; then
|
||||||
|
cmd=$DEMO_COMMENT_COLOR$1$COLOR_RESET
|
||||||
|
else
|
||||||
|
cmd=$DEMO_CMD_COLOR$1$COLOR_RESET
|
||||||
|
fi
|
||||||
|
# wait for the user to press a key before typing the command
|
||||||
|
if [ $NO_WAIT = false ]; then
|
||||||
|
wait
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z $TYPE_SPEED ]]; then
|
||||||
|
echo -en "$cmd"
|
||||||
|
else
|
||||||
|
echo -en "$cmd" | pv -qL $[$TYPE_SPEED+(-2 + RANDOM%5)];
|
||||||
|
fi
|
||||||
|
|
||||||
|
# wait for the user to press a key before moving on
|
||||||
|
if [ $NO_WAIT = false ]; then
|
||||||
|
wait
|
||||||
|
fi
|
||||||
|
sleep $EXEC_WAIT
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# Prints and executes a command
|
||||||
|
#
|
||||||
|
# takes 1 parameter - the string command to run
|
||||||
|
#
|
||||||
|
# usage: pe "ls -l"
|
||||||
|
#
|
||||||
|
##
|
||||||
|
function pe() {
|
||||||
|
# print the command
|
||||||
|
p "$@"
|
||||||
|
run_cmd "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# print and executes a command immediately
|
||||||
|
#
|
||||||
|
# takes 1 parameter - the string command to run
|
||||||
|
#
|
||||||
|
# usage: pei "ls -l"
|
||||||
|
#
|
||||||
|
##
|
||||||
|
function pei {
|
||||||
|
NO_WAIT=true pe "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# Enters script into interactive mode
|
||||||
|
#
|
||||||
|
# and allows newly typed commands to be executed within the script
|
||||||
|
#
|
||||||
|
# usage : cmd
|
||||||
|
#
|
||||||
|
##
|
||||||
|
function cmd() {
|
||||||
|
# render the prompt
|
||||||
|
x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
|
||||||
|
printf "$x\033[0m"
|
||||||
|
read command
|
||||||
|
run_cmd "${command}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_cmd() {
|
||||||
|
function handle_cancel() {
|
||||||
|
printf ""
|
||||||
|
}
|
||||||
|
|
||||||
|
trap handle_cancel SIGINT
|
||||||
|
stty -echoctl
|
||||||
|
eval "$@"
|
||||||
|
stty echoctl
|
||||||
|
trap - SIGINT
|
||||||
|
|
||||||
|
print_prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function check_pv() {
|
||||||
|
command -v pv >/dev/null 2>&1 || {
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}##############################################################"
|
||||||
|
echo "# HOLD IT!! I require pv but it's not installed. Aborting." >&2;
|
||||||
|
echo -e "${RED}##############################################################"
|
||||||
|
echo ""
|
||||||
|
echo -e "${COLOR_RESET}Installing pv:"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Mac:${COLOR_RESET} $ brew install pv"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Other:${COLOR_RESET} http://www.ivarch.com/programs/pv.shtml"
|
||||||
|
echo -e "${COLOR_RESET}"
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check_pv
|
||||||
|
#
|
||||||
|
# handle some default params
|
||||||
|
# -h for help
|
||||||
|
# -d for disabling simulated typing
|
||||||
|
#
|
||||||
|
while getopts ":dhncw:" opt; do
|
||||||
|
case $opt in
|
||||||
|
h)
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
d)
|
||||||
|
unset TYPE_SPEED
|
||||||
|
;;
|
||||||
|
n)
|
||||||
|
NO_WAIT=true
|
||||||
|
;;
|
||||||
|
c)
|
||||||
|
SHOW_CMD_NUMS=true
|
||||||
|
;;
|
||||||
|
w)
|
||||||
|
PROMPT_TIMEOUT=$OPTARG
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
51
scripts/inside
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
. $HOME/demo-magic
|
||||||
|
|
||||||
|
TYPE_SPEED=20
|
||||||
|
EXEC_WAIT=1
|
||||||
|
DEMO_PROMPT="${PURPLE}➜ ${PURPLE}\W "
|
||||||
|
|
||||||
|
defer_kill_htop() {
|
||||||
|
sleep 8
|
||||||
|
pkill htop
|
||||||
|
}
|
||||||
|
|
||||||
|
resize
|
||||||
|
|
||||||
|
print_prompt
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
pei "# Nice auto login ;)"
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=1
|
||||||
|
wait
|
||||||
|
|
||||||
|
if ! $(ps aux|grep -e "sshd: adphi" | grep -v grep &> /dev/null); then
|
||||||
|
pei "# Is the network configured ?"
|
||||||
|
pei "ip a"
|
||||||
|
|
||||||
|
pei "# But is it trully working ?"
|
||||||
|
pei "ping -c 5 linka.cloud"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pei "# Now let's take a look at CPU and Memory usage..."
|
||||||
|
wait
|
||||||
|
|
||||||
|
defer_kill_htop &
|
||||||
|
pei "htop"
|
||||||
|
|
||||||
|
pei "# Let's see disk usage..."
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=3
|
||||||
|
pei "df -hT"
|
||||||
|
wait
|
||||||
|
|
||||||
|
pei "# Pretty small right ? ;)"
|
||||||
|
|
||||||
|
PROMPT_TIMEOUT=1
|
||||||
|
wait
|
||||||
|
|
||||||
|
pei "sudo poweroff"
|
||||||
|
|
100
syslinux.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2023 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const syslinuxCfg = `DEFAULT linux
|
||||||
|
SAY Now booting the kernel from SYSLINUX...
|
||||||
|
LABEL linux
|
||||||
|
KERNEL %s
|
||||||
|
APPEND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
var mbrPaths = []string{
|
||||||
|
// debian path
|
||||||
|
"/usr/lib/syslinux/mbr/mbr.bin",
|
||||||
|
// ubuntu path
|
||||||
|
"/usr/lib/EXTLINUX/mbr.bin",
|
||||||
|
// alpine path
|
||||||
|
"/usr/share/syslinux/mbr.bin",
|
||||||
|
// centos path
|
||||||
|
"/usr/share/syslinux/mbr.bin",
|
||||||
|
// archlinux path
|
||||||
|
"/usr/lib/syslinux/bios/mbr.bin",
|
||||||
|
}
|
||||||
|
|
||||||
|
type syslinux struct {
|
||||||
|
c Config
|
||||||
|
mbrBin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s syslinux) Validate(_ BootFS) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s syslinux) Setup(ctx context.Context, dev, root string, cmdline string) error {
|
||||||
|
logrus.Infof("setting up syslinux bootloader")
|
||||||
|
if err := exec.Run(ctx, "extlinux", "--install", filepath.Join(root, "boot")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "boot", "syslinux.cfg"), []byte(fmt.Sprintf(syslinuxCfg, s.c.Kernel, cmdline)), perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("writing MBR")
|
||||||
|
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", s.mbrBin), fmt.Sprintf("of=%s", dev), "bs=440", "count=1", "conv=notrunc"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type syslinuxProvider struct{}
|
||||||
|
|
||||||
|
func (s syslinuxProvider) New(c Config, _ OSRelease, arch string) (Bootloader, error) {
|
||||||
|
if arch != "x86_64" {
|
||||||
|
return nil, fmt.Errorf("syslinux is only supported for amd64")
|
||||||
|
}
|
||||||
|
mbrBin := ""
|
||||||
|
for _, v := range mbrPaths {
|
||||||
|
if _, err := os.Stat(v); err == nil {
|
||||||
|
mbrBin = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mbrBin == "" {
|
||||||
|
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
|
||||||
|
}
|
||||||
|
return &syslinux{
|
||||||
|
c: c,
|
||||||
|
mbrBin: mbrBin,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s syslinuxProvider) Name() string {
|
||||||
|
return "syslinux"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterBootloaderProvider(syslinuxProvider{})
|
||||||
|
}
|
@ -2,22 +2,49 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apk update --no-cache && \
|
RUN apk add --no-cache \
|
||||||
apk add \
|
|
||||||
util-linux \
|
util-linux \
|
||||||
linux-virt \
|
linux-virt \
|
||||||
|
{{- if ge .Release.VersionID "3.17" }}
|
||||||
|
busybox-openrc \
|
||||||
|
busybox-mdev-openrc \
|
||||||
|
busybox-extras-openrc \
|
||||||
|
busybox-mdev-openrc \
|
||||||
|
{{- else }}
|
||||||
busybox-initscripts \
|
busybox-initscripts \
|
||||||
openrc
|
{{- end }}
|
||||||
|
openrc && \
|
||||||
#RUN apk update --no-cache && \
|
find /boot -type l -exec rm {} \;
|
||||||
# apk add \
|
|
||||||
# linux-virt \
|
|
||||||
# alpine-base \
|
|
||||||
# openssh-server
|
|
||||||
|
|
||||||
RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done
|
RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done
|
||||||
RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
|
RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
|
||||||
|
|
||||||
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
|
{{ if eq .NetworkManager "ifupdown"}}
|
||||||
|
RUN apk add --no-cache ifupdown-ng
|
||||||
|
RUN mkdir -p /etc/network && printf '\
|
||||||
|
auto eth0\n\
|
||||||
|
allow-hotplug eth0\n\
|
||||||
|
iface eth0 inet dhcp\n\
|
||||||
|
' > /etc/network/interfaces
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Luks }}
|
||||||
|
RUN apk add --no-cache cryptsetup && \
|
||||||
|
source /etc/mkinitfs/mkinitfs.conf && \
|
||||||
|
echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \
|
||||||
|
mkinitfs $(ls /lib/modules)
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
# we need to keep that at the end, because after it, we can't install packages without error anymore due to grub hooks
|
||||||
|
{{- if .Grub }}
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
{{- if .GrubBIOS }}
|
||||||
|
grub-bios \
|
||||||
|
{{- end }}
|
||||||
|
{{- if .GrubEFI }}
|
||||||
|
grub-efi \
|
||||||
|
{{- end }}
|
||||||
|
grub
|
||||||
|
{{- end }}
|
||||||
|
@ -2,16 +2,47 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
{{ $version := atoi .Release.Version }}
|
||||||
|
|
||||||
|
{{ if le $version 8 }}
|
||||||
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
||||||
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
|
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
RUN yum update -y
|
RUN yum update -y
|
||||||
|
|
||||||
RUN yum install -y kernel systemd sudo
|
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
|
||||||
|
RUN yum install -y \
|
||||||
|
kernel \
|
||||||
|
systemd \
|
||||||
|
NetworkManager \
|
||||||
|
{{- if .GrubBIOS }}
|
||||||
|
grub2 \
|
||||||
|
{{- end }}
|
||||||
|
{{- if .GrubEFI }}
|
||||||
|
grub2 grub2-efi-x64 grub2-efi-x64-modules \
|
||||||
|
{{- end }}
|
||||||
|
e2fsprogs \
|
||||||
|
sudo && \
|
||||||
|
systemctl enable NetworkManager && \
|
||||||
|
systemctl unmask systemd-remount-fs.service && \
|
||||||
|
systemctl unmask getty.target && \
|
||||||
|
find /boot -type l -exec rm {} \;
|
||||||
|
|
||||||
RUN dracut --no-hostonly --regenerate-all --force && \
|
{{ if .Luks }}
|
||||||
cd /boot && \
|
RUN yum install -y cryptsetup && \
|
||||||
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
|
dracut --no-hostonly --regenerate-all --force --install="/usr/sbin/cryptsetup"
|
||||||
ln -s $(find . -name 'initramfs-*.img') initrd.img
|
{{ else }}
|
||||||
|
RUN dracut --no-hostonly --regenerate-all --force
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
|
{{- if not .Grub }}
|
||||||
|
RUN cd /boot && \
|
||||||
|
mv $(find {{ if le $version 8 }}.{{ else }}/{{ end }} -name 'vmlinuz*') /boot/vmlinuz && \
|
||||||
|
mv $(find . -name 'initramfs-*.img') /boot/initrd.img
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
RUN yum clean all && \
|
||||||
|
rm -rf /var/cache/yum
|
||||||
|
@ -2,18 +2,78 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get -y update && \
|
{{- if eq .Release.VersionID "9" }}
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list && \
|
||||||
linux-image-amd64
|
echo "deb-src http://archive.debian.org/debian stretch main" >> /etc/apt/sources.list && \
|
||||||
|
echo "deb http://archive.debian.org/debian stretch-backports main" >> /etc/apt/sources.list && \
|
||||||
|
echo "deb http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list && \
|
||||||
|
echo "deb-src http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
RUN apt-get update && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
|
linux-image-amd64 && \
|
||||||
|
find /boot -type l -exec rm {} \;
|
||||||
|
|
||||||
|
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
systemd-sysv \
|
systemd-sysv \
|
||||||
systemd \
|
systemd \
|
||||||
|
{{- if .Grub }}
|
||||||
|
grub-common \
|
||||||
|
grub2-common \
|
||||||
|
{{- end }}
|
||||||
|
{{- if .GrubBIOS }}
|
||||||
|
grub-pc-bin \
|
||||||
|
{{- end }}
|
||||||
|
{{- if .GrubEFI }}
|
||||||
|
grub-efi-${ARCH}-bin \
|
||||||
|
{{- end }}
|
||||||
dbus \
|
dbus \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
udhcpc \
|
isc-dhcp-client \
|
||||||
iputils-ping
|
iputils-ping
|
||||||
|
|
||||||
RUN systemctl preset-all
|
RUN systemctl preset-all
|
||||||
|
|
||||||
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
|
{{ if eq .NetworkManager "netplan" }}
|
||||||
|
RUN apt install -y netplan.io
|
||||||
|
RUN mkdir -p /etc/netplan && printf '\
|
||||||
|
network:\n\
|
||||||
|
version: 2\n\
|
||||||
|
renderer: networkd\n\
|
||||||
|
ethernets:\n\
|
||||||
|
eth0:\n\
|
||||||
|
dhcp4: true\n\
|
||||||
|
dhcp-identifier: mac\n\
|
||||||
|
nameservers:\n\
|
||||||
|
addresses:\n\
|
||||||
|
- 8.8.8.8\n\
|
||||||
|
- 8.8.4.4\n\
|
||||||
|
' > /etc/netplan/00-netcfg.yaml
|
||||||
|
{{ else if eq .NetworkManager "ifupdown"}}
|
||||||
|
RUN if [ -z "$(apt-cache madison ifupdown2 2> /dev/nul)" ]; then apt install -y ifupdown; else apt install -y ifupdown2; fi
|
||||||
|
RUN mkdir -p /etc/network && printf '\
|
||||||
|
auto eth0\n\
|
||||||
|
allow-hotplug eth0\n\
|
||||||
|
iface eth0 inet dhcp\n\
|
||||||
|
' > /etc/network/interfaces
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{- if .Luks }}
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
|
||||||
|
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook && \
|
||||||
|
update-initramfs -u -v
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
# needs to be after update-initramfs
|
||||||
|
{{- if not .Grub }}
|
||||||
|
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
|
||||||
|
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
RUN apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -2,19 +2,69 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get update -y && \
|
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
|
||||||
apt-get -y install \
|
apt-get update && \
|
||||||
linux-image-virtual
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
|
linux-image-virtual \
|
||||||
|
initramfs-tools \
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
systemd-sysv \
|
||||||
systemd-sysv \
|
systemd \
|
||||||
systemd \
|
{{- if .Grub }}
|
||||||
dbus \
|
grub-common \
|
||||||
udhcpc \
|
grub2-common \
|
||||||
iproute2 \
|
{{- end }}
|
||||||
iputils-ping
|
{{- if .GrubBIOS }}
|
||||||
|
grub-pc-bin \
|
||||||
|
{{- end }}
|
||||||
|
{{- if .GrubEFI }}
|
||||||
|
grub-efi-${ARCH}-bin \
|
||||||
|
{{- end }}
|
||||||
|
dbus \
|
||||||
|
isc-dhcp-client \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping && \
|
||||||
|
find /boot -type l -exec rm {} \;
|
||||||
|
|
||||||
|
{{ if gt .Release.VersionID "16.04" }}
|
||||||
RUN systemctl preset-all
|
RUN systemctl preset-all
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
|
{{ if eq .NetworkManager "netplan" }}
|
||||||
|
RUN apt install -y netplan.io
|
||||||
|
RUN mkdir -p /etc/netplan && printf '\
|
||||||
|
network:\n\
|
||||||
|
version: 2\n\
|
||||||
|
renderer: networkd\n\
|
||||||
|
ethernets:\n\
|
||||||
|
eth0:\n\
|
||||||
|
dhcp4: true\n\
|
||||||
|
dhcp-identifier: mac\n\
|
||||||
|
nameservers:\n\
|
||||||
|
addresses:\n\
|
||||||
|
- 8.8.8.8\n\
|
||||||
|
- 8.8.4.4\n\
|
||||||
|
' > /etc/netplan/00-netcfg.yaml
|
||||||
|
{{ else if eq .NetworkManager "ifupdown"}}
|
||||||
|
RUN if [ -z "$(apt-cache madison ifupdown-ng 2> /dev/nul)" ]; then apt install -y ifupdown; else apt install -y ifupdown-ng; fi
|
||||||
|
RUN mkdir -p /etc/network && printf '\
|
||||||
|
auto eth0\n\
|
||||||
|
allow-hotplug eth0\n\
|
||||||
|
iface eth0 inet dhcp\n\
|
||||||
|
' > /etc/network/interfaces
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{- if .Luks }}
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
|
||||||
|
update-initramfs -u -v
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
# needs to be after update-initramfs
|
||||||
|
{{- if not .Grub }}
|
||||||
|
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
|
||||||
|
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
RUN apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
28
version.go
@ -1,6 +1,34 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
package d2vm
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/qemu_img"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
Arch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
Version = ""
|
Version = ""
|
||||||
BuildDate = ""
|
BuildDate = ""
|
||||||
|
Image = "linkacloud/d2vm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
qemu_img.DockerImageName = Image
|
||||||
|
qemu_img.DockerImageVersion = Version
|
||||||
|
}
|
||||||
|
2
virtinst
@ -3,4 +3,4 @@
|
|||||||
|
|
||||||
IMG=${1:-disk0.qcow2}
|
IMG=${1:-disk0.qcow2}
|
||||||
|
|
||||||
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'
|
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0' --transient
|
||||||
|