2
0
mirror of https://github.com/linka-cloud/d2vm.git synced 2026-01-25 19:15:04 +00:00

67 Commits

Author SHA1 Message Date
bf88399b58 run/hetzner: rollback sparsecat using outside linux
Makefile: compute tag: ignore dirty state

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-11 04:01:05 +02:00
c97388fdae goreleaser: do not run go generate
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 20:31:16 +02:00
e5dcf8defb Makefile: fix build and release
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 20:10:52 +02:00
badaedc443 goreleaser: enable pre-releases
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:51:56 +02:00
a41be6d27c fix: dockerfile relative path when running in docker
docs: update README.md to current command line api
fix: command line output white for default level

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:36 +02:00
d97b58159c run/hetzner: upload using sparsecat and run e2fsck
docs: add demo scripts

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:36 +02:00
6c93c8be56 tests: fix builder tests: pull image before test
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:36 +02:00
d7f2c453a9 docs: regenerate cli reference
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:36 +02:00
d9f253d65c fix windows build
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
13efc1a646 run/vbox: move console to go.linka.cloud/console
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
c923817c06 run/vbox: improve logging, cleanup on fails, convert to vdi if required
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
35e6aae345 build: fix wrong default Dockerfile path when running in docker
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
9893c8a95a improved commands output: add --time format option and color output
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
77eac66d01 Makefile: docs-up-to-date: fix exclude paths
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:35 +02:00
4763760a1c docs: improved nav
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:34 +02:00
941052b33b docs: fix edit url
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:34 +02:00
7c12ca465a setup documentation site build and deploy 2022-09-10 19:41:34 +02:00
6d8a8d80f5 docs: regenerate docs and sparsecat binary
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:34 +02:00
e767de2c83 docs: update cli reference
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:33 +02:00
480cae12cf feat: add --raw image creation support
refactor: use Option func pattern
fix: build respect the --force flag
fix: compute correct in-docker input and outpout mount paths

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:33 +02:00
eb36d45c35 Makefile: fix docker-push
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:33 +02:00
d0b775ab21 network-manager: fix ifupdown-ng not available, netplan use mac as dhcp identifier
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:33 +02:00
77690dbb57 tests: exclude d2vm_run_qemu.md from diff as defaults change on virtualization features availability
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:33 +02:00
02ca54f141 tests: docs-up-to-date: show diff when failed
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:32 +02:00
82f7d662c7 actions: move docs test to its own job
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:32 +02:00
4720b1cd17 d2vm/run: hetzner: convert image to raw if needed
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:32 +02:00
0192f32905 Makefile: docs up-to-date test: only check docs directory
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:32 +02:00
ecd02424e1 docs: regenerate docs and sparsecat binary
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:32 +02:00
1853fec85a docs: add hidden docs command to generate markdown cli reference
tests: fail if the docs need to be regenerated

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:31 +02:00
7ee4e251e8 d2vm/run: hetzner: do not use sparsecat if not on linux
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:31 +02:00
96026b88ab add verbose flag, deprecate debug false
Dockerfile: add missing ca-certificates
run: hetzner: add token env var
fix examples

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:31 +02:00
1721146c7d network-manager: validate flag value
Dockerfile: fix ubuntu version to 20.04

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:31 +02:00
3417f50e11 docs: add minimum versions [skip ci]
templates: remove extra line continuation

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:31 +02:00
bb4c641a02 chore: add missing copyright headers, remove done todos
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
46494b54c9 tests: run build tests in parallel
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
b09f0e07ad convert / build: add networking support through network-manager flag
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
adbd4c7233 d2vm/run: hetzner: remove server if run is cancelled before beeing created
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
0c24236da9 add "append-to-cmdline" option
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
92cd70430b Makefile: build: add missing docker image
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:30 +02:00
dd1b5006cb d2vm/run: hetzner: use tcp to wait for the server to be ready, do not store server key in UserKnownHostsFile
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:29 +02:00
9f702e5071 d2vm/run: hetzner expand root partition and file system
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:29 +02:00
c7ea09b6a1 goreleaser: ignore commits starting with "tests:"
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:29 +02:00
8b098731d2 tests: split tests run
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:29 +02:00
d2d378ec11 tests: increase timeout
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:29 +02:00
841bf6a7e4 d2vm/run: add hetzner support
tests: add sysconfig tests for the supported distributions

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:28 +02:00
18af3227cc actions: setup tests and releases
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:28 +02:00
598dec4e32 chore: d2vm/run: cleanup unused code, add source reference
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:28 +02:00
56104bbc0f examples/full: add cloud-init support and cloud-guest-utils
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:28 +02:00
6c23c42f80 output defaults to raw
move image using sparsecat
print command in debug mode

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:28 +02:00
5ac3ab9292 run: fix flags not applied
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:27 +02:00
62d8a1019d remove -O option, use output extension instead
add run command to execute vm in qemu or virtualbox

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-09-10 19:41:27 +02:00
29d953c14d fix: policy-rc.d path typo
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-08-12 13:42:55 +02:00
2af13ef626 sysconfig: fix paths for ubuntu versions before 20.04 (fix #3)
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-08-04 17:06:27 +02:00
Adphi
0d4379946b Merge pull request #1 from cyrinux/fix/archlinux-mbr-path
fix: add archlinux mbr.bin path
2022-07-21 19:50:04 +02:00
Cyril Levis
e9f3ac9193 fix: add archlinux mbr.bin path 2022-07-20 20:31:36 +02:00
a40b7d3c07 d2vm: fix CentOS install
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-26 14:59:33 +02:00
8538bb0521 Dockefile: remove udevadm missing warning
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-24 16:58:15 +02:00
13d913db38 d2vm: remove kpartx dependency, use parted instead of fdisk
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-24 16:48:21 +02:00
085e57a07a refactoring: explicit docker commands
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-24 16:27:04 +02:00
20ba409039 d2vm: flatten docker image using github.com/google/go-containerregistry
This allows to preserve files like /etc/hostname or /etc/resolv.conf that will otherwise be overriden by running the container to extract rootfs

wip img entrypoint script

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-24 15:49:01 +02:00
0c9bfb6dd8 README.md: add badges
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-22 13:26:48 +02:00
8c1455b030 doc: add asciinema
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-22 12:35:26 +02:00
690f697ee0 d2vm: smaller ubuntu base images
full example: enable serial auto login

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-21 22:24:54 +02:00
fa3a4f6039 d2vm: remove non working rhel support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-21 21:31:57 +02:00
1a97b45861 d2vm: add version command, lookup mbr.bin in well-known paths
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-21 21:15:39 +02:00
a21fb68b7b fix typo (again)
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-21 19:13:46 +02:00
04bf1810e2 fix typo
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2022-04-21 18:37:33 +02:00
71 changed files with 5567 additions and 337 deletions

View File

@@ -3,3 +3,7 @@ tests
disk*
qemu.sh
**/*.qcow2
bin
dist
images
examples/build

276
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,276 @@
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.18
- 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
- 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: make tests
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.18
- 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.18
- 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
with:
gpg_private_key: ${{ secrets.GPG_KEY }}
passphrase: ${{ secrets.GPG_PASSWORD }}
- name: Build Snapshot
run: make build-snapshot
- name: Release Snapshot
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-18.04
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.18
- 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-18.04
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- docs-up-to-date
- build
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.18
- 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-18.04
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- docs-up-to-date
- build-image
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.18
- 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

22
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Docs
on:
push:
branches:
- docs
- main
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

9
.gitignore vendored
View File

@@ -1,6 +1,15 @@
.idea
tests
scratch
*.qcow2
*.vmdk
*.vdi
bin/
dist/
images
/d2vm
/examples/build
.goreleaser.yaml
docs/build
docs-src

50
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,50 @@
# 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
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
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^tests:'
- '^actions:'
- '^Makefile:'
- '^chore:'
- '^goreleaser:'

View File

@@ -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.
FROM golang as builder
WORKDIR /d2vm
@@ -9,20 +23,20 @@ RUN go mod download
COPY . .
RUN go build -o d2vm ./cmd/d2vm
RUN make .build
FROM ubuntu
FROM ubuntu:20.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
ca-certificates \
util-linux \
kpartx \
udev \
parted \
e2fsprogs \
xfsprogs \
mount \
tar \
extlinux \
uuid-runtime \
qemu-utils
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/

100
Makefile
View File

@@ -16,20 +16,42 @@ MODULE = go.linka.cloud/d2vm
REPOSITORY = linkacloud
TAG = $(shell git diff --quiet && git describe --tags --exact-match 2> /dev/null)
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)
show-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: docker-build 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 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 --rm -i -t \
@@ -38,3 +60,77 @@ docker-run:
-v $(PWD):/build \
-w /build \
$(DOCKER_IMAGE) bash
.PHONY: tests
tests:
@go generate ./...
@go list ./...| xargs go test -exec sudo -count=1 -timeout 20m -v
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 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
.build:
@go generate ./...
@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: examples
examples: build-dev
@mkdir -p examples/build
@for f in $$(find examples -type f -name '*Dockerfile' -maxdepth 1); do \
echo "Building $$f"; \
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -f $$f examples; \
done
@echo "Building examples/full/Dockerfile"
@./d2vm build -o examples/build/full.qcow2 --build-arg=USER=adphi --build-arg=PASSWORD=adphi examples/full
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

142
README.md
View File

@@ -1,5 +1,10 @@
# 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*
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,66 +13,56 @@ Many thanks to him.
**Status**: *alpha*
[![asciicast](https://asciinema.org/a/520132.svg)](https://asciinema.org/a/520132)
## 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:
```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"
```
**Starting from v0.1.0, d2vm automatically run build and convert commands inside Docker when not running on linux**.
## Supported VM Linux distributions:
Working and tested:
- [x] Ubuntu
- [x] Debian
- [x] Ubuntu (18.04+)
- [x] Debian (stretch+)
- [x] Alpine
- [x] CentOS (8+)
Need fix:
Unsupported:
- [ ] CentOS / RHEL
- [ ] RHEL
The program use the `/etc/os-release` file to discovery 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.
Obviously, **Distroless** images are not supported.
## Getting started
### Install from release
Download the latest release for your platform from the [release page](https://github.com/linka-cloud/d2vm/releases/latest)
### Install from source
Clone the git repository:
```bash
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
go install ./cmd/d2vm
which d2vm
```
```
# Should be install in the $GOBIN directory
/go/bin/d2vm
```
Or use an alias to the **docker** image:
```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"
which d2vm
```
```
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
make build-dev && sudo cp d2vm /usr/local/bin/
```
### Converting an existing Docker Image to VM image:
```bash
b2vm convert --help
d2vm convert --help
```
```
Convert Docker image to vm image
@@ -76,14 +71,20 @@ Usage:
d2vm convert [docker image] [flags]
Flags:
-d, --debug Enable Debug output
-f, --force Override output qcow2 image
-h, --help help for convert
-o, --output string The output image (default "disk0.qcow2")
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
-p, --password string The Root user password (default "root")
--pull Always pull docker image
-s, --size string The output image size (default "10G")
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
-f, --force Override output qcow2 image
-h, --help help for convert
--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 The Root user password (default "root")
--pull Always pull docker image
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
Global Flags:
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
@@ -93,27 +94,27 @@ Create an image based on the **ubuntu** official image:
sudo d2vm convert ubuntu -o ubuntu.qcow2 -p MyP4Ssw0rd
```
```
INFO[0000] pulling image ubuntu
INFO[0001] inspecting image ubuntu
INFO[0002] docker image based on Ubuntu
INFO[0002] building kernel enabled image
INFO[0038] creating root file system archive
INFO[0040] creating vm image
INFO[0040] creating raw image
INFO[0040] mounting raw image
INFO[0040] creating raw image file system
INFO[0040] copying rootfs to raw image
INFO[0041] setting up rootfs
INFO[0041] installing linux kernel
INFO[0042] unmounting raw image
INFO[0042] writing MBR
INFO[0042] converting to qcow2
Pulling image ubuntu
Inspecting image ubuntu
No network manager specified, using distribution defaults: netplan
Docker image based on Ubuntu 22.04.1 LTS (Jammy Jellyfish)
Building kernel enabled image
Creating vm image
Creating raw image
Mounting raw image
Creating raw image file system
Copying rootfs to raw image
Setting up rootfs
Installing linux kernel
Unmounting raw image
Writing MBR
Converting to qcow2
```
You can now run your ubuntu image using the created `ubuntu.qcow2` image with **qemu**:
```bash
./qemu.sh ununtu.qcow2
d2vm run qemu ubuntu.qcow2
```
```
SeaBIOS (version 1.13.0-1ubuntu1.1)
@@ -185,7 +186,7 @@ applicable law.
root@localhost:~#
```
Type `poweroff` to shutdown the vm.
Type `poweroff` to shut down the vm.
### Building a VM Image from a Dockerfile
@@ -201,7 +202,7 @@ cd examples
FROM ubuntu
RUN apt update && apt install -y openssh-server && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
```
@@ -222,15 +223,20 @@ Usage:
d2vm build [context directory] [flags]
Flags:
--build-arg stringArray Set build-time variables
-d, --debug Enable Debug output
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile') (default "Dockerfile")
--force Override output image
-h, --help help for build
-o, --output string The output image (default "disk0.qcow2")
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
-p, --password string Root user password (default "root")
-s, --size string The output image size (default "10G")
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output image
-h, --help help for build
--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 Root user password (default "root")
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
Global Flags:
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
@@ -241,7 +247,7 @@ sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.qcow2 .
Or if you want to create a VirtualBox image:
```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 .
```
### Complete example
@@ -257,4 +263,8 @@ You can find the Dockerfiles used to install the Kernel in the [templates](templ
- [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ?
- [ ] 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).

View File

@@ -15,10 +15,8 @@
package d2vm
import (
"bytes"
"context"
"fmt"
"math"
"os"
exec2 "os/exec"
"path/filepath"
@@ -46,39 +44,72 @@ ff02::3 ip6-allhosts
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
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
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
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
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
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200 %s
`
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
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
mbrBin = "/usr/lib/EXTLINUX/mbr.bin"
)
var (
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
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",
}
)
const (
perm os.FileMode = 0644
)
func sysconfig(osRelease OSRelease) (string, error) {
switch osRelease.ID {
case ReleaseUbuntu:
if osRelease.VersionID < "20.04" {
return syslinuxCfgDebian, nil
}
return syslinuxCfgUbuntu, nil
case ReleaseDebian:
return syslinuxCfgDebian, nil
case ReleaseAlpine:
return syslinuxCfgAlpine, nil
case ReleaseCentOS:
return syslinuxCfgCentOS, nil
default:
return "", fmt.Errorf("%s: distribution not supported", osRelease.ID)
}
}
type builder struct {
osRelease OSRelease
src string
img *image
diskRaw string
diskOut string
format string
@@ -86,12 +117,15 @@ type builder struct {
size int64
mntPoint string
loDevice string
loPart string
diskUUD string
mbrPath string
loDevice string
loPart string
diskUUD string
cmdLineExtra 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 int64, osRelease OSRelease, format string, cmdLineExtra string) (*builder, error) {
if err := checkDependencies(); err != nil {
return nil, err
}
@@ -105,29 +139,46 @@ func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, form
if !valid {
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
}
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")
}
if size == 0 {
size = 10 * int64(datasize.GB)
}
if disk == "" {
disk = "disk0"
}
i, err := os.Stat(src)
img, err := NewImage(ctx, imgTag, workdir)
if err != nil {
return nil, err
}
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)
}
// i, err := os.Stat(imgTar)
// if err != nil {
// return nil, err
// }
// 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{
osRelease: osRelease,
src: src,
diskRaw: filepath.Join(workdir, disk+".raw"),
diskOut: filepath.Join(workdir, disk+".qcow2"),
format: f,
size: size,
mntPoint: filepath.Join(workdir, "/mnt"),
osRelease: osRelease,
img: img,
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
diskOut: filepath.Join(workdir, disk+"."+format),
format: f,
size: size,
mbrPath: mbrBin,
mntPoint: filepath.Join(workdir, "/mnt"),
cmdLineExtra: cmdLineExtra,
}
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err
@@ -193,18 +244,9 @@ func (b *builder) makeImg(ctx context.Context) error {
if err := block(b.diskRaw, b.size); err != nil {
return err
}
c := exec.CommandContext(ctx, "fdisk", b.diskRaw)
var i bytes.Buffer
for _, v := range fdiskCmds {
if _, err := i.Write([]byte(v + "\n")); err != nil {
return err
}
}
var e bytes.Buffer
c.Stdin = &i
c.Stderr = &e
if err := c.Run(); err != nil {
return fmt.Errorf("%w: %s", err, e.String())
if err := exec.Run(ctx, "parted", "-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"); err != nil {
return err
}
return nil
}
@@ -216,10 +258,10 @@ func (b *builder) mountImg(ctx context.Context) error {
return err
}
b.loDevice = strings.TrimSuffix(o, "\n")
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
if err := exec.Run(ctx, "partprobe", b.loDevice); err != nil {
return err
}
b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
b.loPart = fmt.Sprintf("%sp1", b.loDevice)
logrus.Infof("creating raw image file system")
if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil {
return err
@@ -236,9 +278,6 @@ func (b *builder) unmountImg(ctx context.Context) error {
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
merr = multierr.Append(merr, err)
}
if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
@@ -247,7 +286,7 @@ func (b *builder) unmountImg(ctx context.Context) error {
func (b *builder) copyRootFS(ctx context.Context) error {
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 nil
@@ -261,19 +300,20 @@ func (b *builder) setupRootFS(ctx context.Context) error {
}
b.diskUUD = strings.TrimSuffix(o, "\n")
fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD)
if err := b.chWriteFile("/etc/fstab", fstab, 0644); err != nil {
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
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
}
if err := b.chWriteFile("/etc/hostname", "localhost", 0644); err != nil {
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
return err
}
if err := b.chWriteFile("/etc/hosts", hosts, 0644); err != nil {
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
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
}
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
@@ -287,10 +327,10 @@ func (b *builder) setupRootFS(ctx context.Context) error {
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 {
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
return err
}
if err := b.chWriteFile("/etc/network/interfaces", "", 0644); err != nil {
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
return err
}
return nil
@@ -301,20 +341,11 @@ func (b *builder) installKernel(ctx context.Context) error {
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
return err
}
var sysconfig string
switch b.osRelease.ID {
case ReleaseUbuntu:
sysconfig = syslinuxCfgUbuntu
case ReleaseDebian:
sysconfig = syslinuxCfgDebian
case ReleaseAlpine:
sysconfig = syslinuxCfgAlpine
case ReleaseCentOS, ReleaseRHEL:
sysconfig = syslinuxCfgCentOS
default:
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
sysconfig, err := sysconfig(b.osRelease)
if err != nil {
return err
}
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), 0644); err != nil {
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD, b.cmdLineExtra), perm); err != nil {
return err
}
return nil
@@ -322,7 +353,7 @@ func (b *builder) installKernel(ctx context.Context) error {
func (b *builder) setupMBR(ctx context.Context) error {
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", mbrBin), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil {
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 err
}
return nil
@@ -337,10 +368,21 @@ func (b *builder) chWriteFile(path string, content string, perm os.FileMode) err
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 {
return fmt.Sprintf("%s%s", b.mntPoint, path)
}
func (b *builder) Close() error {
return b.img.Close()
}
func block(path string, size int64) error {
f, err := os.Create(path)
if err != nil {
@@ -352,14 +394,11 @@ func block(path string, size int64) error {
func checkDependencies() error {
var merr error
for _, v := range []string{"mount", "blkid", "tar", "kpartx", "losetup", "qemu-img", "extlinux", "dd", "mkfs", "fdisk"} {
for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "partprobe", "qemu-img", "extlinux", "dd", "mkfs"} {
if _, err := exec2.LookPath(v); err != nil {
merr = multierr.Append(merr, err)
}
}
if _, err := os.Stat(mbrBin); err != nil {
merr = multierr.Append(merr, err)
}
return merr
}

148
builder_test.go Normal file
View File

@@ -0,0 +1,148 @@
// 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/assert"
"github.com/stretchr/testify/require"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, initrd string) {
require.NoError(t, docker.Pull(ctx, img))
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(img))
require.NoError(t, os.MkdirAll(tmpPath, 0755))
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
require.NoError(t, err)
defer docker.Remove(ctx, img)
sys, err := sysconfig(r)
require.NoError(t, err)
assert.Equal(t, sysconf, sys)
d, err := NewDockerfile(r, img, "root", "")
require.NoError(t, 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)
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, imgUUID, p, dir))
defer docker.Remove(ctx, imgUUID)
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", kernel))
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", initrd))
}
func TestSyslinuxCfg(t *testing.T) {
t.Parallel()
tests := []struct {
image string
kernel string
initrd string
sysconfig string
}{
{
image: "ubuntu:18.04",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "ubuntu:20.04",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
},
{
image: "ubuntu:22.04",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
},
{
image: "ubuntu:latest",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
},
{
image: "debian:9",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "debian:10",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "debian:11",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "debian:latest",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "alpine",
kernel: "/boot/vmlinuz-virt",
initrd: "/boot/initramfs-virt",
sysconfig: syslinuxCfgAlpine,
},
{
image: "centos:8",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
},
{
image: "centos:latest",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
},
}
exec.SetDebug(true)
for _, test := range tests {
test := test
t.Run(test.image, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testSysconfig(t, ctx, test.image, test.sysconfig, test.kernel, test.initrd)
})
}
}

View File

@@ -15,6 +15,10 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/google/uuid"
@@ -23,34 +27,85 @@ import (
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
var (
file = "Dockerfile"
tag = uuid.New().String()
buildArgs []string
buildCmd = &cobra.Command{
file = "Dockerfile"
tag = "d2vm-" + uuid.New().String()
networkManager string
buildArgs []string
buildCmd = &cobra.Command{
Use: "build [context directory]",
Short: "Build a vm image from Dockerfile",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// TODO(adphi): resolve context path
if runtime.GOOS != "linux" {
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:]...)
}
size, err := parseSize(size)
if err != nil {
return err
}
if debug {
exec.Run = exec.RunStdout
if file == "" {
file = filepath.Join(args[0], "Dockerfile")
}
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
if !force {
return fmt.Errorf("%s already exists", output)
}
}
logrus.Infof("building docker image from %s", file)
dargs := []string{"build", "-t", tag, "-f", file, args[0]}
for _, v := range buildArgs {
dargs = append(dargs, "--build-arg", v)
}
if err := docker.Cmd(cmd.Context(), dargs...); err != nil {
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
return err
}
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
return d2vm.Convert(
cmd.Context(),
tag,
d2vm.WithSize(size),
d2vm.WithPassword(password),
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithRaw(raw),
)
},
}
)
@@ -58,13 +113,14 @@ var (
func init() {
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().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
buildCmd.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(), " "))
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")
buildCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
buildCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
buildCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
}

View File

@@ -17,6 +17,8 @@ package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/c2h5oh/datasize"
@@ -25,11 +27,12 @@ import (
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
var (
pull = false
raw bool
pull = false
cmdLineExtra = ""
convertCmd = &cobra.Command{
Use: "convert [docker image]",
@@ -37,6 +40,21 @@ var (
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if runtime.GOOS != "linux" {
abs, err := filepath.Abs(output)
if err != nil {
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...)
}
img := args[0]
tag := "latest"
if parts := strings.Split(img, ":"); len(parts) > 1 {
@@ -51,32 +69,33 @@ var (
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
if !pull {
o, _, err := docker.CmdOut(cmd.Context(), "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", img)
imgs, err := docker.ImageList(cmd.Context(), img)
if err != nil {
return err
}
found = strings.TrimSuffix(o, "\n") == fmt.Sprintf("%s:%s", img, tag)
found = len(imgs) == 1 && imgs[0] == fmt.Sprintf("%s:%s", img, tag)
if found {
logrus.Infof("using local image %s:%s", img, tag)
}
}
if pull || !found {
logrus.Infof("pulling image %s", img)
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
if err := docker.Pull(cmd.Context(), img); err != nil {
return err
}
}
return d2vm.Convert(cmd.Context(), img, size, password, output, format)
return d2vm.Convert(
cmd.Context(),
img,
d2vm.WithSize(size),
d2vm.WithPassword(password),
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithRaw(raw),
)
},
}
)
@@ -91,11 +110,12 @@ func parseSize(s string) (int64, error) {
func init() {
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
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(&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(), " "))
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")
convertCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
convertCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
convertCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
rootCmd.AddCommand(convertCmd)
}

43
cmd/d2vm/docs.go Normal file
View 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)
}

View File

@@ -15,22 +15,48 @@
package main
import (
"bytes"
"context"
"fmt"
"os"
"os/signal"
"strings"
"time"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/exec"
)
var (
output = "disk0.qcow2"
size = "1G"
password = "root"
force = false
debug = false
format = "qcow2"
output = "disk0.qcow2"
size = "1G"
password = "root"
force = false
verbose = false
timeFormat = ""
format = "qcow2"
rootCmd = &cobra.Command{
Use: "d2vm",
SilenceUsage: true,
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)
},
}
)
@@ -38,5 +64,66 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, os.Kill)
go func() {
<-sigs
fmt.Println()
cancel()
}()
rootCmd.ExecuteContext(ctx)
}
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().StringVarP(&timeFormat, "time", "t", "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
}

36
cmd/d2vm/run.go Normal file
View 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
View File

@@ -0,0 +1 @@
Shamelessly taken from [linuxkit](https://github.com/linuxkit/linuxkit/tree/master/src/cmd/linuxkit)

352
cmd/d2vm/run/hetzner.go Normal file
View File

@@ -0,0 +1,352 @@
// 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"
"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"
exec2 "go.linka.cloud/d2vm/pkg/exec"
)
const (
hetznerTokenEnv = "HETZNER_TOKEN"
serverImg = "ubuntu-20.04"
vmBlockPath = "/dev/sda"
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")
}
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 := ImgInfo(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 := exec2.Run(ctx, "qemu-img", "convert", "-O", "raw", imgPath, rawPath); err != nil {
return err
}
imgPath = rawPath
i, err = ImgInfo(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
}
img, _, err := c.Image.GetByName(ctx, serverImg)
if err != nil {
return err
}
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: img,
Location: l,
StartAfterCreate: hcloud.Bool(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(sparsecatBinary)); 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()
}
}
gses, err := sc.NewSession()
if err != nil {
return err
}
defer gses.Close()
logrus.Infof("resizing disk partition")
cmd := fmt.Sprintf("growpart %s 1", vmBlockPath)
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 %s1", vmBlockPath)
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 %s1", vmBlockPath)
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
}

448
cmd/d2vm/run/qemu.go Normal file
View File

@@ -0,0 +1,448 @@
package run
import (
"crypto/rand"
"fmt"
"net"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
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
qemuCmd string
qemuDetached bool
networking string
publishFlags MultipleFlag
deviceFlags MultipleFlag
usbEnabled bool
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]")
flags.StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file")
// 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")
// 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 [])")
// USB devices
flags.BoolVar(&usbEnabled, "usb", false, "Enable USB controller")
flags.Var(&deviceFlags, "device", "Add USB host device(s). Format driver[,prop=value][,...] -- add device, like -device on the qemu command line.")
}
func Qemu(cmd *cobra.Command, args []string) {
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
vmUUID := 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, "")
path := args[0]
if _, err := os.Stat(path); err != nil {
log.Fatal(err)
}
for i, d := range 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 == "" {
log.Fatalf("disk specified with no size or name")
}
disks[i] = d
}
disks = append(Disks{DiskConfig{Path: path}}, disks...)
if networking == "" || networking == "default" {
dflt := qemuNetworkingDefault
networking = dflt
}
netMode := strings.SplitN(networking, ",", 2)
var netdevConfig string
switch netMode[0] {
case qemuNetworkingUser:
netdevConfig = "user,id=t0"
case qemuNetworkingTap:
if len(netMode) != 2 {
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingTap)
}
if len(publishFlags) != 0 {
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
}
netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
case qemuNetworkingBridge:
if len(netMode) != 2 {
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingBridge)
}
if len(publishFlags) != 0 {
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
}
netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
case qemuNetworkingNone:
if len(publishFlags) != 0 {
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
}
netdevConfig = ""
default:
log.Fatalf("Invalid networking mode: %s", netMode[0])
}
config := QemuConfig{
Path: path,
GUI: enableGUI,
Disks: disks,
Arch: arch,
CPUs: cpus,
Memory: mem,
Accel: accel,
Detached: qemuDetached,
QemuBinPath: qemuCmd,
PublishedPorts: publishFlags,
NetdevConfig: netdevConfig,
UUID: vmUUID,
USB: usbEnabled,
Devices: deviceFlags,
}
config, err := discoverBinaries(config)
if err != nil {
log.Fatal(err)
}
if err = runQemuLocal(config); err != nil {
log.Fatal(err.Error())
}
}
func runQemuLocal(config QemuConfig) error {
var args []string
config, args = buildQemuCmdline(config)
for _, d := range config.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(config.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 config.Detached == true {
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
}
qemuCmd := exec.Command(config.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 config.GUI != true {
qemuCmd.Stdin = os.Stdin
qemuCmd.Stdout = os.Stdout
qemuCmd.Stderr = os.Stderr
}
return qemuCmd.Run()
}
func buildQemuCmdline(config QemuConfig) (QemuConfig, []string) {
// Iterate through the flags and build arguments
var qemuArgs []string
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", config.CPUs))
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", config.Memory))
qemuArgs = append(qemuArgs, "-uuid", config.UUID.String())
// 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 config.Arch == "aarch64" {
if runtime.GOARCH == "arm64" {
qemuArgs = append(qemuArgs, "-cpu", "host")
} else {
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
}
}
// goArch is the GOARCH equivalent of config.Arch
var goArch string
switch config.Arch {
case "s390x":
goArch = "s390x"
case "aarch64":
goArch = "arm64"
case "x86_64":
goArch = "amd64"
default:
log.Fatalf("%s is an unsupported architecture.", config.Arch)
}
if goArch != runtime.GOARCH {
log.Infof("Disable acceleration as %s != %s", config.Arch, runtime.GOARCH)
config.Accel = ""
}
if config.Accel != "" {
switch config.Arch {
case "s390x":
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", config.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, config.Accel))
default:
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", config.Accel))
}
} else {
switch config.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 config.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 config.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 config.NetdevConfig == "" {
qemuArgs = append(qemuArgs, "-net", "none")
} else {
mac := generateMAC()
if config.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(config.PublishedPorts)
if err != nil {
log.Error(err)
}
qemuArgs = append(qemuArgs, "-netdev", config.NetdevConfig+forwardings)
}
if config.GUI != true {
qemuArgs = append(qemuArgs, "-nographic")
}
if config.USB == true {
qemuArgs = append(qemuArgs, "-usb")
}
for _, d := range config.Devices {
qemuArgs = append(qemuArgs, "-device", d)
}
return config, qemuArgs
}
func discoverBinaries(config QemuConfig) (QemuConfig, error) {
if config.QemuImgPath != "" {
return config, nil
}
qemuBinPath := "qemu-system-" + config.Arch
qemuImgPath := "qemu-img"
var err error
config.QemuBinPath, err = exec.LookPath(qemuBinPath)
if err != nil {
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
}
config.QemuImgPath, err = exec.LookPath(qemuImgPath)
if err != nil {
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
}
return config, nil
}
func buildQemuForwardings(publishFlags MultipleFlag) (string, error) {
if len(publishFlags) == 0 {
return "", nil
}
var forwardings string
for _, publish := range publishFlags {
p, err := NewPublishedPort(publish)
if err != nil {
return "", err
}
hostPort := p.Host
guestPort := p.Guest
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
}
return forwardings, nil
}
func buildDockerForwardings(publishedPorts []string) ([]string, error) {
pmap := []string{}
for _, port := range publishedPorts {
s, err := NewPublishedPort(port)
if err != nil {
return nil, err
}
pmap = append(pmap, "-p", fmt.Sprintf("%d:%d/%s", s.Host, s.Guest, s.Protocol))
}
return pmap, nil
}
// QemuConfig contains the config for Qemu
type QemuConfig struct {
Path string
GUI bool
Disks Disks
FWPath string
Arch string
CPUs uint
Memory uint
Accel string
Detached bool
QemuBinPath string
QemuImgPath string
PublishedPorts []string
NetdevConfig string
UUID uuid.UUID
USB bool
Devices []string
}
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 net.HardwareAddr(mac)
}

Binary file not shown.

362
cmd/d2vm/run/util.go Normal file
View File

@@ -0,0 +1,362 @@
//go:generate env GOOS=linux GOARCH=amd64 go build -o sparsecat-linux-amd64 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"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
//go:embed sparsecat-linux-amd64
var sparsecatBinary []byte
// 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
}
// DiskConfig is the config for a disk
type DiskConfig struct {
Path string
Size int
Format string
}
// Disks is the type for a list of DiskConfig
type Disks []DiskConfig
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 := DiskConfig{}
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
}
type QemuInfo 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 ImgInfo(ctx context.Context, path string) (*QemuInfo, error) {
o, err := exec.CommandContext(ctx, "qemu-img", "info", path, "--output", "json").CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%v: %s", err, string(o))
}
var i QemuInfo
if err := json.Unmarshal(o, &i); err != nil {
return nil, err
}
return &i, nil
}

347
cmd/d2vm/run/vbox.go Normal file
View File

@@ -0,0 +1,347 @@
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"
exec2 "go.linka.cloud/d2vm/pkg/exec"
)
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 := ImgInfo(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 := exec2.Run(ctx, "qemu-img", "convert", "-O", "vdi", path, vdi); err != nil {
return err
}
path = vdi
}
// remove machine in case it already exists
cleanup(vboxmanage, name)
_, 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) {
if _, _, err := manage(vboxmanage, "controlvm", name, "poweroff"); err != nil {
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 {
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 {
log.Errorf("storageattach error: %v\n%s", err, out)
}
}
if _, out, err = manage(vboxmanage, "unregistervm", name, "--delete"); err != nil {
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
}

37
cmd/d2vm/version.go Normal file
View File

@@ -0,0 +1,37 @@
// 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"
"github.com/spf13/cobra"
"go.linka.cloud/d2vm"
)
var (
cmdVersion = &cobra.Command{
Use: "version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(d2vm.Version)
fmt.Println(d2vm.BuildDate)
},
}
)
func init() {
rootCmd.AddCommand(cmdVersion)
}

View File

@@ -17,17 +17,22 @@ package d2vm
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/svenwiltink/sparsecat"
"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()
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
@@ -40,51 +45,52 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu
if err != nil {
return err
}
d, err := NewDockerfile(r, img, password)
if err != nil {
return err
if !o.raw {
d, err := NewDockerfile(r, img, o.password, o.networkManager)
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, imgUUID, p, dir); err != nil {
return err
}
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
}
defer docker.Remove(ctx, imgUUID)
}
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)
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)
if err != nil {
return err
}
defer b.Close()
if err := b.Build(ctx); err != nil {
return err
}
if err := os.RemoveAll(output); err != nil {
if err := os.RemoveAll(o.output); err != nil {
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 nil
@@ -101,7 +107,7 @@ func MoveFile(sourcePath, destPath string) error {
return fmt.Errorf("failed to open dest file: %s", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
_, err = sparsecat.NewDecoder(sparsecat.NewEncoder(inputFile)).WriteTo(outputFile)
inputFile.Close()
if err != nil {
return fmt.Errorf("failed to write to output file: %s", err)

62
convert_options.go Normal file
View File

@@ -0,0 +1,62 @@
// 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 int64
password string
output string
cmdLineExtra string
networkManager NetworkManager
raw bool
}
func WithSize(size int64) 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 WithRaw(raw bool) ConvertOption {
return func(o *convertOptions) {
o.raw = raw
}
}

View File

@@ -15,58 +15,122 @@
package d2vm
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/google/go-containerregistry/cmd/crane/cmd"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"go.linka.cloud/d2vm/pkg/exec"
)
const (
dockerImageRun = `
#!/bin/sh
{{- range .Config.Env }}
{{- range .DockerImageConfig.Env }}
export {{ . }}
{{- 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 (
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 {
Config struct {
Hostname string `json:"Hostname"`
Domainname string `json:"Domainname"`
User string `json:"User"`
AttachStdin bool `json:"AttachStdin"`
AttachStdout bool `json:"AttachStdout"`
AttachStderr bool `json:"AttachStderr"`
ExposedPorts struct {
Tcp struct {
} `json:"3000/tcp"`
} `json:"ExposedPorts"`
Tty bool `json:"Tty"`
OpenStdin bool `json:"OpenStdin"`
StdinOnce bool `json:"StdinOnce"`
Env []string `json:"Env"`
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"`
DockerImageConfig `json:"Config"`
Architecture string `json:"Architecture"`
Os string `json:"Os"`
Size int `json:"Size"`
}
type DockerImageConfig struct {
Image string `json:"Image"`
Hostname string `json:"Hostname"`
Domainname string `json:"Domainname"`
User string `json:"User"`
Env []string `json:"Env"`
Cmd []string `json:"Cmd"`
WorkingDir string `json:"WorkingDir"`
Entrypoint []string `json:"Entrypoint"`
}
func (i DockerImage) AsRunScript(w io.Writer) error {
return dockerImageRunTemplate.Execute(w, i)
}
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
ref, err := name.ParseReference(tag)
if err != nil {
return nil, err
}
img, err := daemon.Image(ref)
if err != nil {
return nil, err
}
if err := os.MkdirAll(imageTmpPath, perm); 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, perm); 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
View 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, perm))
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, img, "", tmp))
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)
}

View File

@@ -19,6 +19,8 @@ import (
"fmt"
"io"
"text/template"
"github.com/sirupsen/logrus"
)
//go:embed templates/ubuntu.Dockerfile
@@ -40,33 +42,71 @@ var (
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").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 {
Image string
Password string
Release OSRelease
tmpl *template.Template
Image string
Password string
Release OSRelease
NetworkManager NetworkManager
tmpl *template.Template
}
func (d Dockerfile) Render(w io.Writer) error {
return d.tmpl.Execute(w, d)
}
func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error) {
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager) (Dockerfile, error) {
if password == "" {
password = "root"
}
d := Dockerfile{Release: release, Image: img, Password: password}
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager}
var net NetworkManager
switch release.ID {
case ReleaseDebian:
d.tmpl = debianDockerfileTemplate
net = NetworkManagerIfupdown2
case ReleaseUbuntu:
d.tmpl = ubuntuDockerfileTemplate
net = NetworkManagerNetplan
case ReleaseAlpine:
d.tmpl = alpineDockerfileTemplate
case ReleaseCentOS, ReleaseRHEL:
net = NetworkManagerIfupdown2
if networkManager == NetworkManagerNetplan {
return d, fmt.Errorf("netplan is not supported on alpine")
}
case ReleaseCentOS:
d.tmpl = centOSDockerfileTemplate
net = NetworkManagerNone
if networkManager != "" && networkManager != NetworkManagerNone {
return Dockerfile{}, fmt.Errorf("network manager is not supported on centos")
}
default:
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
}

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View 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

View File

@@ -0,0 +1 @@
../../examples/full/README.md

1
docs/content/index.md Symbolic link
View File

@@ -0,0 +1 @@
../../README.md

View File

@@ -0,0 +1,20 @@
## d2vm
### Options
```
-h, --help help for d2vm
-t, --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) -

View File

@@ -0,0 +1,34 @@
## 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
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output image
-h, --help help for build
--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 Root user password (default "root")
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
```
### Options inherited from parent commands
```
-t, --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) -

View 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
```
-t, --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

View 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 > /usr/local/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
```
-t, --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

View 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
```
-t, --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

View 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
```
-t, --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

View File

@@ -0,0 +1,48 @@
## 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 for every new session, execute once:
#### Linux:
d2vm completion zsh > "${fpath[1]}/_d2vm"
#### macOS:
d2vm completion zsh > /usr/local/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
```
-t, --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

View File

@@ -0,0 +1,33 @@
## 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
-f, --force Override output qcow2 image
-h, --help help for convert
--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 The Root user password (default "root")
--pull Always pull docker image
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
```
### Options inherited from parent commands
```
-t, --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) -

View File

@@ -0,0 +1,24 @@
## d2vm run
Run the virtual machine image
### Options
```
-h, --help help for run
```
### Options inherited from parent commands
```
-t, --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

View File

@@ -0,0 +1,30 @@
## d2vm run hetzner
Run the virtual machine image on Hetzner Cloud
```
d2vm run hetzner [options] image-path [flags]
```
### Options
```
-h, --help help for hetzner
-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]
-u, --user string d2vm image ssh user (default "root")
```
### Options inherited from parent commands
```
-t, --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

View File

@@ -0,0 +1,38 @@
## 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 "hvf:tcg")
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
--cpus uint Number of CPUs (default 1)
--data string String of metadata to pass to VM; error to specify both -data and -data-file
--detached Set qemu container to run in the background
--device multiple-flag Add USB host device(s). Format driver[,prop=value][,...] -- add device, like -device on the qemu command line. (default A multiple flag is a type of flag that can be repeated any number of times)
--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)
--usb Enable USB controller
```
### Options inherited from parent commands
```
-t, --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

View 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
```
-t, --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

View File

@@ -0,0 +1,25 @@
## d2vm version
```
d2vm version [flags]
```
### Options
```
-h, --help help for version
```
### Options inherited from parent commands
```
-t, --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
View 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 &copy; 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

View File

@@ -1,4 +1,5 @@
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

View File

@@ -1,10 +0,0 @@
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: true
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4

View File

@@ -1,15 +1,13 @@
FROM ubuntu
# Install netplan sudo ssh-server and dns utils
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
ameu-guest-agent \
netplan.io \
# Install some system packages
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
qemu-guest-agent \
ca-certificates \
dnsutils \
sudo \
openssh-server
# Setup default network config
COPY 00-netconf.yaml /etc/netplan/
# Add a utility script to resize serial terminal
COPY resize /usr/local/bin/
@@ -19,13 +17,17 @@ ARG PASSWORD=d2vm
ARG SSH_KEY=https://github.com/${USER}.keys
# Setup user environment
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
bash-completion \
curl \
zsh \
git \
vim \
tmux \
htop
htop \
lsb-core \
cloud-init \
cloud-guest-utils
# Create user with sudo privileged and passwordless sudo
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
@@ -44,4 +46,5 @@ USER ${USER}
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
# Setup tmux environment
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

View File

@@ -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
FROM ubuntu
# Install netplan sudo ssh-server and dns utils
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
ameu-guest-agent \
netplan.io \
# Install some system packages
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
qemu-guest-agent \
ca-certificates \
dnsutils \
sudo \
openssh-server
# Setup default network config
COPY 00-netconf.yaml /etc/netplan/
# Add a utility script to resize serial terminal
COPY resize /usr/local/bin/
@@ -25,13 +23,17 @@ ARG PASSWORD=d2vm
ARG SSH_KEY=https://github.com/${USER}.keys
# Setup user environment
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
bash-completion \
curl \
zsh \
git \
vim \
tmux \
htop
htop \
lsb-core \
cloud-init \
cloud-guest-utils
# Create user with sudo privileged and passwordless 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)"
# Setup tmux environment
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
```
*00-netconf.yaml*
```yaml
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: true
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4
# 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
```
There is no need to configure the network as **d2vm** will generate a *netplan* configuration that use DHCP.
**Build**
```bash
@@ -74,22 +66,29 @@ USER=mygithubuser
PASSWORD=mysecurepasswordthatIwillneverusebecauseIuseMostlySSHkeys
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
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
virsh domifaddr --domain workstation
hostname -I
# or
ip a show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1
```
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...*

View File

@@ -1,4 +1,4 @@
FROM ubuntu
RUN apt update && apt install -y openssh-server && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config

53
go.mod
View File

@@ -4,16 +4,67 @@ go 1.17
require (
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0
github.com/google/go-containerregistry v0.8.0
github.com/google/uuid v1.3.0
github.com/hetznercloud/hcloud-go v1.35.2
github.com/joho/godotenv v1.4.0
github.com/pkg/sftp v1.10.1
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.0
github.com/svenwiltink/sparsecat v1.0.0
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
go.uber.org/multierr v1.8.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/containerd v1.5.8 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.12+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/kr/fs v0.1.0 // 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.2-0.20181231171920-c182affec369 // 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.0.2-0.20211117181255-693428a734f5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

1364
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -46,8 +46,10 @@ func (r Release) Supported() bool {
return true
case ReleaseAlpine:
return true
case ReleaseCentOS, ReleaseRHEL:
case ReleaseCentOS:
return true
case ReleaseRHEL:
return false
default:
return false
}
@@ -91,7 +93,7 @@ var (
)
func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) {
d := filepath.Join(tmpPath, "osrelase.Dockerfile")
d := filepath.Join(tmpPath, "osrelease.Dockerfile")
f, err := os.Create(d)
if err != nil {
return OSRelease{}, err

View File

@@ -15,13 +15,27 @@
package docker
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"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 {
s := strings.Replace(name, ":", "-", -1)
s = strings.Replace(s, "/", "_", -1)
@@ -35,3 +49,96 @@ func Cmd(ctx context.Context, args ...string) error {
func CmdOut(ctx context.Context, args ...string) (string, string, error) {
return exec.RunOut(ctx, "docker", args...)
}
func Build(ctx context.Context, tag, dockerfile, dir string, buildArgs ...string) error {
if dockerfile == "" {
dockerfile = filepath.Join(dir, "Dockerfile")
}
args := []string{"image", "build", "-t", tag, "-f", dockerfile}
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 Pull(ctx context.Context, tag string) error {
return Cmd(ctx, "image", "pull", tag)
}
func RunInteractiveAndRemove(ctx context.Context, args ...string) error {
logrus.Tracef("running 'docker run --rm -i -t %s'", strings.Join(args, " "))
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{
"--privileged",
"-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,
}
return RunInteractiveAndRemove(ctx, append(a, args...)...)
}

View File

@@ -18,9 +18,10 @@ import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/sirupsen/logrus"
)
var (
@@ -29,10 +30,20 @@ var (
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 RunDebug(ctx context.Context, c string, args ...string) error {
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
cmd := exec.CommandContext(ctx, c, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
return cmd.Run()
}

75
scripts/demo Executable file
View 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
View 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
View 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"

View File

@@ -9,15 +9,16 @@ RUN apk update --no-cache && \
busybox-initscripts \
openrc
#RUN apk update --no-cache && \
# 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 devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
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 }}

View File

@@ -7,7 +7,10 @@ RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
RUN yum update -y
RUN yum install -y kernel systemd sudo
RUN yum install -y kernel systemd NetworkManager e2fsprogs sudo && \
systemctl enable NetworkManager && \
systemctl unmask systemd-remount-fs.service && \
systemctl unmask getty.target
RUN dracut --no-hostonly --regenerate-all --force && \
cd /boot && \

View File

@@ -11,9 +11,33 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
systemd \
dbus \
iproute2 \
udhcpc \
isc-dhcp-client \
iputils-ping
RUN systemctl preset-all
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
{{ 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 }}

View File

@@ -3,18 +3,40 @@ FROM {{ .Image }}
USER root
RUN apt-get update -y && \
apt-get -y install \
linux-image-virtual
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
systemd-sysv \
systemd \
dbus \
udhcpc \
iproute2 \
iputils-ping
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-virtual \
initramfs-tools \
systemd-sysv \
systemd \
dbus \
isc-dhcp-client \
iproute2 \
iputils-ping
RUN systemctl preset-all
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
{{ 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 }}

21
version.go Normal file
View File

@@ -0,0 +1,21 @@
// 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
var (
Version = ""
BuildDate = ""
Image = ""
)

View File

@@ -3,4 +3,4 @@
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