Compare commits
63 Commits
v0.0.2
...
v0.1.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
c97388fdae
|
|||
|
e5dcf8defb
|
|||
|
badaedc443
|
|||
|
a41be6d27c
|
|||
|
d97b58159c
|
|||
|
6c93c8be56
|
|||
|
d7f2c453a9
|
|||
|
d9f253d65c
|
|||
|
13efc1a646
|
|||
|
c923817c06
|
|||
|
35e6aae345
|
|||
|
9893c8a95a
|
|||
|
77eac66d01
|
|||
|
4763760a1c
|
|||
|
941052b33b
|
|||
|
7c12ca465a
|
|||
|
6d8a8d80f5
|
|||
|
e767de2c83
|
|||
|
480cae12cf
|
|||
|
eb36d45c35
|
|||
|
d0b775ab21
|
|||
|
77690dbb57
|
|||
|
02ca54f141
|
|||
|
82f7d662c7
|
|||
|
4720b1cd17
|
|||
|
0192f32905
|
|||
|
ecd02424e1
|
|||
|
1853fec85a
|
|||
|
7ee4e251e8
|
|||
|
96026b88ab
|
|||
|
1721146c7d
|
|||
|
3417f50e11
|
|||
|
bb4c641a02
|
|||
|
46494b54c9
|
|||
|
b09f0e07ad
|
|||
|
adbd4c7233
|
|||
|
0c24236da9
|
|||
|
92cd70430b
|
|||
|
dd1b5006cb
|
|||
|
9f702e5071
|
|||
|
c7ea09b6a1
|
|||
|
8b098731d2
|
|||
|
d2d378ec11
|
|||
|
841bf6a7e4
|
|||
|
18af3227cc
|
|||
|
598dec4e32
|
|||
|
56104bbc0f
|
|||
|
6c23c42f80
|
|||
|
5ac3ab9292
|
|||
|
62d8a1019d
|
|||
|
29d953c14d
|
|||
|
2af13ef626
|
|||
|
|
0d4379946b | ||
|
|
e9f3ac9193 | ||
|
a40b7d3c07
|
|||
|
8538bb0521
|
|||
|
13d913db38
|
|||
|
085e57a07a
|
|||
|
20ba409039
|
|||
|
0c9bfb6dd8
|
|||
|
8c1455b030
|
|||
|
690f697ee0
|
|||
|
fa3a4f6039
|
@@ -3,3 +3,7 @@ tests
|
||||
disk*
|
||||
qemu.sh
|
||||
**/*.qcow2
|
||||
bin
|
||||
dist
|
||||
images
|
||||
examples/build
|
||||
|
||||
276
.github/workflows/ci.yaml
vendored
Normal 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
@@ -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
@@ -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
@@ -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:'
|
||||
24
Dockerfile
@@ -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 make build
|
||||
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/
|
||||
|
||||
99
Makefile
@@ -16,20 +16,42 @@ MODULE = go.linka.cloud/d2vm
|
||||
|
||||
REPOSITORY = linkacloud
|
||||
|
||||
TAG = $(shell 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 \
|
||||
@@ -39,5 +61,76 @@ docker-run:
|
||||
-w /build \
|
||||
$(DOCKER_IMAGE) bash
|
||||
|
||||
build:
|
||||
.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
@@ -1,5 +1,10 @@
|
||||
|
||||
# d2vm (Docker to Virtual Machine)
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://pkg.go.dev/go.linka.cloud/d2vm)
|
||||
[](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*
|
||||
|
||||
[](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 discover the Linux distribution and install the Kernel,
|
||||
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
||||
if the file is missing, the build cannot succeed.
|
||||
|
||||
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).
|
||||
|
||||
148
builder.go
@@ -15,10 +15,8 @@
|
||||
package d2vm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
exec2 "os/exec"
|
||||
"path/filepath"
|
||||
@@ -46,31 +44,29 @@ 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
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
|
||||
|
||||
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
|
||||
|
||||
mbrPaths = []string{
|
||||
@@ -82,13 +78,38 @@ var (
|
||||
"/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
|
||||
@@ -98,12 +119,13 @@ type builder struct {
|
||||
|
||||
mbrPath string
|
||||
|
||||
loDevice string
|
||||
loPart string
|
||||
diskUUD 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
|
||||
}
|
||||
@@ -134,24 +156,29 @@ func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, form
|
||||
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,
|
||||
mbrPath: mbrBin,
|
||||
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
|
||||
@@ -217,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
|
||||
}
|
||||
@@ -240,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
|
||||
@@ -260,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)
|
||||
}
|
||||
@@ -271,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
|
||||
@@ -285,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 {
|
||||
@@ -311,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
|
||||
@@ -325,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
|
||||
@@ -361,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 {
|
||||
@@ -376,7 +394,7 @@ 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)
|
||||
}
|
||||
|
||||
148
builder_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -15,25 +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)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -41,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
@@ -0,0 +1,36 @@
|
||||
// Copyright 2022 Linka Cloud All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.linka.cloud/d2vm/cmd/d2vm/run"
|
||||
)
|
||||
|
||||
var (
|
||||
runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run the virtual machine image",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
|
||||
runCmd.AddCommand(run.VboxCmd)
|
||||
runCmd.AddCommand(run.QemuCmd)
|
||||
runCmd.AddCommand(run.HetznerCmd)
|
||||
}
|
||||
1
cmd/d2vm/run/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Shamelessly taken from [linuxkit](https://github.com/linuxkit/linuxkit/tree/master/src/cmd/linuxkit)
|
||||
347
cmd/d2vm/run/hetzner.go
Normal file
@@ -0,0 +1,347 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
cmd := fmt.Sprintf("%s -r -disable-sparse-target -of %s", sparsecatPath, 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
@@ -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)
|
||||
}
|
||||
BIN
cmd/d2vm/run/sparsecat-linux-amd64
Executable file
362
cmd/d2vm/run/util.go
Normal 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
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 Linka Cloud All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
82
convert.go
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
126
docker_image.go
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
BIN
docs/content/assets/d2vm-dark-b.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
docs/content/assets/d2vm-dark-tr.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/content/assets/d2vm-dark.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/content/assets/d2vm-favicon.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
docs/content/assets/d2vm-light-tr.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/content/assets/d2vm-light.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
docs/content/assets/d2vm.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
1
docs/content/assets/matrixorg-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 66 56" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M49.46 37.36h-5.32c-.178 0-.323-.145-.323-.323V26.06l-.074-1.808c-.047-.53-.173-.992-.376-1.376a2.2 2.2 0 0 0-.868-.883c-.38-.22-.93-.332-1.62-.332s-1.238.13-1.647.382-.743.597-.976 1a4.21 4.21 0 0 0-.486 1.462c-.085.567-.128 1.15-.128 1.732v10.8c0 .178-.145.323-.323.323H32c-.178 0-.323-.145-.323-.323V26.18l-.037-1.7c-.024-.524-.124-1.013-.297-1.45-.164-.415-.43-.74-.814-.992s-.972-.378-1.752-.378c-.22 0-.527.053-.908.157-.368.1-.732.294-1.08.577s-.65.694-.904 1.235-.382 1.27-.382 2.167v11.24c0 .178-.144.323-.323.323h-5.32c-.178 0-.323-.145-.323-.323v-19.37c0-.178.145-.322.323-.322h5.02c.178 0 .323.145.323.322v1.794c.618-.726 1.33-1.315 2.125-1.757 1.032-.574 2.225-.865 3.548-.865 1.265 0 2.44.25 3.5.743.934.44 1.68 1.17 2.224 2.18.556-.703 1.263-1.34 2.108-1.895 1.036-.682 2.274-1.028 3.68-1.028 1.048 0 2.036.13 2.937.387.917.263 1.715.7 2.373 1.267s1.18 1.348 1.548 2.278c.363.922.547 2.04.547 3.323v12.964c0 .178-.145.323-.323.323z" opacity=".5"/><path d="M24.88 17.675v2.623h.075c.7-.998 1.542-1.774 2.53-2.323s2.117-.824 3.39-.824c1.224 0 2.342.238 3.353.712s1.78 1.31 2.305 2.51c.574-.85 1.355-1.6 2.342-2.248s2.154-.974 3.504-.974c1.024 0 1.973.125 2.848.375s1.623.65 2.248 1.2 1.11 1.268 1.462 2.154.525 1.955.525 3.204v12.964h-5.32V26.07l-.075-1.836c-.05-.574-.187-1.073-.412-1.5s-.556-.762-.993-1.012-1.03-.374-1.78-.374-1.355.145-1.817.43a3.12 3.12 0 0 0-1.087 1.124c-.263.461-.437.987-.524 1.574s-.132 1.184-.131 1.78v10.79H32V26.182l-.037-1.705c-.025-.562-.13-1.08-.32-1.556s-.5-.855-.937-1.143-1.08-.43-1.93-.43c-.25 0-.58.056-.993.17a3.3 3.3 0 0 0-1.199.637c-.388.313-.718.762-.993 1.35s-.412 1.355-.412 2.304v11.24h-5.32V17.675z" opacity=".5"/><path d="M1.432 1.244v51.833h3.73v1.244H0V0h5.162v1.243zm20.788 16.43v2.623h.075c.7-.998 1.542-1.774 2.53-2.323s2.117-.824 3.4-.824c1.224 0 2.342.238 3.353.712s1.78 1.3 2.305 2.5c.574-.85 1.355-1.6 2.342-2.248s2.154-.974 3.504-.974c1.024 0 1.973.125 2.848.375s1.623.65 2.248 1.2 1.1 1.268 1.462 2.154.525 1.955.525 3.204v12.964h-5.32V26.06l-.075-1.836c-.05-.574-.187-1.073-.412-1.5s-.556-.762-.993-1.012-1.03-.374-1.78-.374-1.355.145-1.817.43a3.12 3.12 0 0 0-1.087 1.124c-.263.46-.437.987-.524 1.574s-.132 1.184-.131 1.78v10.8h-5.32V26.182l-.037-1.705c-.025-.562-.13-1.08-.32-1.556s-.5-.855-.937-1.143-1.08-.43-1.93-.43c-.25 0-.58.056-.993.17a3.3 3.3 0 0 0-1.199.637c-.388.313-.718.762-.993 1.35s-.412 1.355-.412 2.304v11.24H17.2V17.675zm40.348 35.402V1.244h-3.73V0H64v54.32h-5.162v-1.244z" fill="#000"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
docs/content/full-example.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../../examples/full/README.md
|
||||
1
docs/content/index.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../../README.md
|
||||
20
docs/content/reference/d2vm.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## d2vm
|
||||
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for d2vm
|
||||
-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) -
|
||||
|
||||
34
docs/content/reference/d2vm_build.md
Normal 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) -
|
||||
|
||||
31
docs/content/reference/d2vm_completion.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## d2vm completion
|
||||
|
||||
Generate the autocompletion script for the specified shell
|
||||
|
||||
### Synopsis
|
||||
|
||||
Generate the autocompletion script for d2vm for the specified shell.
|
||||
See each sub-command's help for details on how to use the generated script.
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for completion
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
|
||||
|
||||
50
docs/content/reference/d2vm_completion_bash.md
Normal file
@@ -0,0 +1,50 @@
|
||||
## d2vm completion bash
|
||||
|
||||
Generate the autocompletion script for bash
|
||||
|
||||
### Synopsis
|
||||
|
||||
Generate the autocompletion script for the bash shell.
|
||||
|
||||
This script depends on the 'bash-completion' package.
|
||||
If it is not installed already, you can install it via your OS's package manager.
|
||||
|
||||
To load completions in your current shell session:
|
||||
|
||||
source <(d2vm completion bash)
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
|
||||
#### Linux:
|
||||
|
||||
d2vm completion bash > /etc/bash_completion.d/d2vm
|
||||
|
||||
#### macOS:
|
||||
|
||||
d2vm completion bash > /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
|
||||
|
||||
41
docs/content/reference/d2vm_completion_fish.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## d2vm completion fish
|
||||
|
||||
Generate the autocompletion script for fish
|
||||
|
||||
### Synopsis
|
||||
|
||||
Generate the autocompletion script for the fish shell.
|
||||
|
||||
To load completions in your current shell session:
|
||||
|
||||
d2vm completion fish | source
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
|
||||
d2vm completion fish > ~/.config/fish/completions/d2vm.fish
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
```
|
||||
d2vm completion fish [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for fish
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
|
||||
|
||||
38
docs/content/reference/d2vm_completion_powershell.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## d2vm completion powershell
|
||||
|
||||
Generate the autocompletion script for powershell
|
||||
|
||||
### Synopsis
|
||||
|
||||
Generate the autocompletion script for powershell.
|
||||
|
||||
To load completions in your current shell session:
|
||||
|
||||
d2vm completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
To load completions for every new session, add the output of the above command
|
||||
to your powershell profile.
|
||||
|
||||
|
||||
```
|
||||
d2vm completion powershell [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for powershell
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
|
||||
|
||||
48
docs/content/reference/d2vm_completion_zsh.md
Normal 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
|
||||
|
||||
33
docs/content/reference/d2vm_convert.md
Normal 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) -
|
||||
|
||||
24
docs/content/reference/d2vm_run.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## d2vm run
|
||||
|
||||
Run the virtual machine image
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for run
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
|
||||
|
||||
30
docs/content/reference/d2vm_run_hetzner.md
Normal 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
|
||||
|
||||
38
docs/content/reference/d2vm_run_qemu.md
Normal 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
|
||||
|
||||
32
docs/content/reference/d2vm_run_vbox.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## d2vm run vbox
|
||||
|
||||
Run the virtual machine image with Virtualbox
|
||||
|
||||
```
|
||||
d2vm run vbox [options] image-path [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--cpus uint Number of CPUs (default 1)
|
||||
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=raw] (default [])
|
||||
--gui Show the VM GUI
|
||||
-h, --help help for vbox
|
||||
--mem uint Amount of memory in MB (default 1024)
|
||||
--name string Name of the Virtualbox VM (default "d2vm")
|
||||
--networking vbnetworks Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>] (default [])
|
||||
--vboxmanage string VBoxManage binary to use (default "VBoxManage")
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
|
||||
|
||||
25
docs/content/reference/d2vm_version.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## d2vm version
|
||||
|
||||
|
||||
|
||||
```
|
||||
d2vm version [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for version
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-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
@@ -0,0 +1,43 @@
|
||||
site_name: ""
|
||||
docs_dir: content
|
||||
site_dir: build
|
||||
edit_uri: edit/docs/docs/content/
|
||||
theme:
|
||||
name: linka-cloud
|
||||
logo: assets/d2vm-light-tr.png
|
||||
favicon: assets/d2vm-favicon.png
|
||||
language: en
|
||||
repo_url: https://github.com/linka-cloud/d2vm
|
||||
copyright: Copyright © 2022 Linka Cloud
|
||||
nav:
|
||||
- Getting Started: index.md
|
||||
- Complete Example: full-example.md
|
||||
- Command Line:
|
||||
- d2vm: reference/d2vm.md
|
||||
- build: reference/d2vm_build.md
|
||||
- convert: reference/d2vm_convert.md
|
||||
- run:
|
||||
- hetzner: reference/d2vm_run_hetzner.md
|
||||
- qemu: reference/d2vm_run_qemu.md
|
||||
- virtualbox: reference/d2vm_run_vbox.md
|
||||
- completion:
|
||||
- bash: reference/d2vm_completion_bash.md
|
||||
- fish: reference/d2vm_completion_fish.md
|
||||
- powershell: reference/d2vm_completion_powershell.md
|
||||
- zsh: reference/d2vm_completion_zsh.md
|
||||
- version: reference/d2vm_version.md
|
||||
|
||||
extra:
|
||||
homepage: https://github.com/linka-cloud/d2vm
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/linka-cloud
|
||||
- icon: fontawesome/brands/docker
|
||||
link: https://hub.docker.com/r/linkacloud
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.highlight:
|
||||
use_pygments: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
network:
|
||||
version: 2
|
||||
renderer: networkd
|
||||
ethernets:
|
||||
eth0:
|
||||
dhcp4: true
|
||||
nameservers:
|
||||
addresses:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
@@ -1,15 +1,13 @@
|
||||
FROM ubuntu
|
||||
|
||||
# Install netplan sudo ssh-server and dns utils
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
# Install some system packages
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
qemu-guest-agent \
|
||||
netplan.io \
|
||||
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,14 +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 && \
|
||||
@@ -45,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
|
||||
|
||||
@@ -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 \
|
||||
# Install some system packages
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
qemu-guest-agent \
|
||||
netplan.io \
|
||||
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...*
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)...)
|
||||
}
|
||||
|
||||
@@ -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
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir="$PWD"
|
||||
scripts_dir="scripts"
|
||||
if [ "$(basename $PWD)" == "$scripts_dir" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
. ./$scripts_dir/demo-magic
|
||||
|
||||
TYPE_SPEED=20
|
||||
EXEC_WAIT=1
|
||||
DEMO_PROMPT="${CYAN}➜ ${CYAN}\W "
|
||||
clear
|
||||
|
||||
IMAGE="./images/workstation.qcow2"
|
||||
|
||||
|
||||
PROMPT_TIMEOUT=1
|
||||
print_prompt
|
||||
wait
|
||||
pei "# Let's create a virtual machine from a Dockerfile"
|
||||
wait
|
||||
|
||||
DOCKERFILE="examples/full/Dockerfile"
|
||||
|
||||
pei "cat $DOCKERFILE"
|
||||
|
||||
cp scripts/demo-magic examples/full
|
||||
cp scripts/inside examples/full
|
||||
cat <<EOF >> $DOCKERFILE
|
||||
COPY demo-magic /home/adphi/demo-magic
|
||||
COPY inside /home/adphi/inside
|
||||
RUN sudo chmod +x /home/adphi/inside && echo /home/adphi/inside >> /home/adphi/.zshrc && sudo apt install -y pv
|
||||
EOF
|
||||
|
||||
|
||||
PROMPT_TIMEOUT=5
|
||||
wait
|
||||
PROMPT_TIMEOUT=0
|
||||
|
||||
EXEC_WAIT=2
|
||||
pei "export PASSWORD=\"Don'tThinkTh4tIReallyUseThisPassword:)\""
|
||||
pei "sudo d2vm build -s 10G -o $IMAGE --force --build-arg USER=adphi --build-arg PASSWORD=\$PASSWORD -p \$PASSWORD -v --time=relative examples/full"
|
||||
|
||||
rm examples/full/{demo-magic,inside}
|
||||
git checkout examples/ &> /dev/null
|
||||
|
||||
PROMPT_TIMEOUT=1
|
||||
wait
|
||||
PROMPT_TIMEOUT=2
|
||||
EXEC_WAIT=1
|
||||
pei "# Now let's run this image"
|
||||
wait
|
||||
|
||||
EXEC_WAIT=2
|
||||
pei "sudo d2vm run qemu --cpus 4 --mem 4096 --networking default $IMAGE"
|
||||
|
||||
# demo continues inside the vm is soon as the boot completes
|
||||
|
||||
wait
|
||||
|
||||
EXEC_WAIT=1
|
||||
|
||||
pei "# Let's try to run it on a cloud provider: Hetzner..."
|
||||
|
||||
EXEC_WAIT=2
|
||||
pei "sudo -E d2vm run hetzner --rm -v --time=relative -u adphi -i ~/.ssh/id_rsa $IMAGE"
|
||||
|
||||
# demo continues inside the vm is soon as the boot completes
|
||||
|
||||
pei "# Pretty cool rigth ? :)"
|
||||
wait
|
||||
|
||||
cd $dir
|
||||
220
scripts/demo-magic
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# demo-magic.sh
|
||||
#
|
||||
# Copyright (c) 2015 Paxton Hare
|
||||
#
|
||||
# This script lets you script demos in bash. It runs through your demo script when you press
|
||||
# ENTER. It simulates typing and runs commands.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# the speed to "type" the text
|
||||
TYPE_SPEED=20
|
||||
|
||||
# no wait after "p" or "pe"
|
||||
NO_WAIT=false
|
||||
|
||||
# if > 0, will pause for this amount of seconds before automatically proceeding with any p or pe
|
||||
PROMPT_TIMEOUT=0
|
||||
|
||||
# don't show command number unless user specifies it
|
||||
SHOW_CMD_NUMS=false
|
||||
|
||||
EXEC_WAIT=1
|
||||
|
||||
|
||||
# handy color vars for pretty prompts
|
||||
BLACK="\033[0;30m"
|
||||
BLUE="\033[0;34m"
|
||||
GREEN="\033[0;32m"
|
||||
GREY="\033[0;90m"
|
||||
CYAN="\033[0;36m"
|
||||
RED="\033[0;31m"
|
||||
PURPLE="\033[0;35m"
|
||||
BROWN="\033[0;33m"
|
||||
WHITE="\033[1;37m"
|
||||
COLOR_RESET="\033[0m"
|
||||
|
||||
C_NUM=0
|
||||
|
||||
# prompt and command color which can be overriden
|
||||
DEMO_PROMPT="$ "
|
||||
DEMO_CMD_COLOR=$WHITE
|
||||
DEMO_COMMENT_COLOR=$GREY
|
||||
|
||||
##
|
||||
# prints the script usage
|
||||
##
|
||||
function usage() {
|
||||
echo -e ""
|
||||
echo -e "Usage: $0 [options]"
|
||||
echo -e ""
|
||||
echo -e "\tWhere options is one or more of:"
|
||||
echo -e "\t-h\tPrints Help text"
|
||||
echo -e "\t-d\tDebug mode. Disables simulated typing"
|
||||
echo -e "\t-n\tNo wait"
|
||||
echo -e "\t-w\tWaits max the given amount of seconds before proceeding with demo (e.g. '-w5')"
|
||||
echo -e ""
|
||||
}
|
||||
|
||||
##
|
||||
# wait for user to press ENTER
|
||||
# if $PROMPT_TIMEOUT > 0 this will be used as the max time for proceeding automatically
|
||||
##
|
||||
function wait() {
|
||||
if [[ "$PROMPT_TIMEOUT" == "0" ]]; then
|
||||
read -rs
|
||||
else
|
||||
read -rst "$PROMPT_TIMEOUT"
|
||||
fi
|
||||
}
|
||||
|
||||
print_prompt() {
|
||||
# render the prompt
|
||||
x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
|
||||
|
||||
# show command number is selected
|
||||
if $SHOW_CMD_NUMS; then
|
||||
printf "[$((++C_NUM))] $x"
|
||||
else
|
||||
printf "$x"
|
||||
fi
|
||||
}
|
||||
|
||||
##
|
||||
# print command only. Useful for when you want to pretend to run a command
|
||||
#
|
||||
# takes 1 param - the string command to print
|
||||
#
|
||||
# usage: p "ls -l"
|
||||
#
|
||||
##
|
||||
function p() {
|
||||
if [[ ${1:0:1} == "#" ]]; then
|
||||
cmd=$DEMO_COMMENT_COLOR$1$COLOR_RESET
|
||||
else
|
||||
cmd=$DEMO_CMD_COLOR$1$COLOR_RESET
|
||||
fi
|
||||
# wait for the user to press a key before typing the command
|
||||
if [ $NO_WAIT = false ]; then
|
||||
wait
|
||||
fi
|
||||
|
||||
if [[ -z $TYPE_SPEED ]]; then
|
||||
echo -en "$cmd"
|
||||
else
|
||||
echo -en "$cmd" | pv -qL $[$TYPE_SPEED+(-2 + RANDOM%5)];
|
||||
fi
|
||||
|
||||
# wait for the user to press a key before moving on
|
||||
if [ $NO_WAIT = false ]; then
|
||||
wait
|
||||
fi
|
||||
sleep $EXEC_WAIT
|
||||
echo ""
|
||||
}
|
||||
|
||||
##
|
||||
# Prints and executes a command
|
||||
#
|
||||
# takes 1 parameter - the string command to run
|
||||
#
|
||||
# usage: pe "ls -l"
|
||||
#
|
||||
##
|
||||
function pe() {
|
||||
# print the command
|
||||
p "$@"
|
||||
run_cmd "$@"
|
||||
}
|
||||
|
||||
##
|
||||
# print and executes a command immediately
|
||||
#
|
||||
# takes 1 parameter - the string command to run
|
||||
#
|
||||
# usage: pei "ls -l"
|
||||
#
|
||||
##
|
||||
function pei {
|
||||
NO_WAIT=true pe "$@"
|
||||
}
|
||||
|
||||
##
|
||||
# Enters script into interactive mode
|
||||
#
|
||||
# and allows newly typed commands to be executed within the script
|
||||
#
|
||||
# usage : cmd
|
||||
#
|
||||
##
|
||||
function cmd() {
|
||||
# render the prompt
|
||||
x=$(PS1="$DEMO_PROMPT" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
|
||||
printf "$x\033[0m"
|
||||
read command
|
||||
run_cmd "${command}"
|
||||
}
|
||||
|
||||
function run_cmd() {
|
||||
function handle_cancel() {
|
||||
printf ""
|
||||
}
|
||||
|
||||
trap handle_cancel SIGINT
|
||||
stty -echoctl
|
||||
eval "$@"
|
||||
stty echoctl
|
||||
trap - SIGINT
|
||||
|
||||
print_prompt
|
||||
}
|
||||
|
||||
|
||||
function check_pv() {
|
||||
command -v pv >/dev/null 2>&1 || {
|
||||
|
||||
echo ""
|
||||
echo -e "${RED}##############################################################"
|
||||
echo "# HOLD IT!! I require pv but it's not installed. Aborting." >&2;
|
||||
echo -e "${RED}##############################################################"
|
||||
echo ""
|
||||
echo -e "${COLOR_RESET}Installing pv:"
|
||||
echo ""
|
||||
echo -e "${BLUE}Mac:${COLOR_RESET} $ brew install pv"
|
||||
echo ""
|
||||
echo -e "${BLUE}Other:${COLOR_RESET} http://www.ivarch.com/programs/pv.shtml"
|
||||
echo -e "${COLOR_RESET}"
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
|
||||
check_pv
|
||||
#
|
||||
# handle some default params
|
||||
# -h for help
|
||||
# -d for disabling simulated typing
|
||||
#
|
||||
while getopts ":dhncw:" opt; do
|
||||
case $opt in
|
||||
h)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
d)
|
||||
unset TYPE_SPEED
|
||||
;;
|
||||
n)
|
||||
NO_WAIT=true
|
||||
;;
|
||||
c)
|
||||
SHOW_CMD_NUMS=true
|
||||
;;
|
||||
w)
|
||||
PROMPT_TIMEOUT=$OPTARG
|
||||
;;
|
||||
esac
|
||||
done
|
||||
51
scripts/inside
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
. $HOME/demo-magic
|
||||
|
||||
TYPE_SPEED=20
|
||||
EXEC_WAIT=1
|
||||
DEMO_PROMPT="${PURPLE}➜ ${PURPLE}\W "
|
||||
|
||||
defer_kill_htop() {
|
||||
sleep 8
|
||||
pkill htop
|
||||
}
|
||||
|
||||
resize
|
||||
|
||||
print_prompt
|
||||
|
||||
sleep 2
|
||||
|
||||
pei "# Nice auto login ;)"
|
||||
|
||||
PROMPT_TIMEOUT=1
|
||||
wait
|
||||
|
||||
if ! $(ps aux|grep -e "sshd: adphi" | grep -v grep &> /dev/null); then
|
||||
pei "# Is the network configured ?"
|
||||
pei "ip a"
|
||||
|
||||
pei "# But is it trully working ?"
|
||||
pei "ping -c 5 linka.cloud"
|
||||
fi
|
||||
|
||||
pei "# Now let's take a look at CPU and Memory usage..."
|
||||
wait
|
||||
|
||||
defer_kill_htop &
|
||||
pei "htop"
|
||||
|
||||
pei "# Let's see disk usage..."
|
||||
|
||||
PROMPT_TIMEOUT=3
|
||||
pei "df -hT"
|
||||
wait
|
||||
|
||||
pei "# Pretty small right ? ;)"
|
||||
|
||||
PROMPT_TIMEOUT=1
|
||||
wait
|
||||
|
||||
pei "sudo poweroff"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
15
version.go
@@ -1,6 +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 = ""
|
||||
)
|
||||
|
||||
2
virtinst
@@ -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
|
||||
|
||||