2
0
mirror of https://github.com/linka-cloud/d2vm.git synced 2024-11-14 03:56:25 +00:00

Compare commits

..

No commits in common. "main" and "v0.0.3" have entirely different histories.
main ... v0.0.3

88 changed files with 521 additions and 6694 deletions

View File

@ -3,9 +3,3 @@ tests
disk*
qemu.sh
**/*.qcow2
bin
dist
images
examples/build
e2e
**/*_test.go

View File

@ -1,376 +0,0 @@
name: Tests and Build
on:
push:
branches: [ "*" ]
tags: [ "v*" ]
pull_request:
branches: [ main ]
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || make tests
templates-tests:
name: Test Templates
runs-on: ubuntu-latest
strategy:
matrix:
image:
- ubuntu
- debian
- kalilinux
- alpine
- centos
- quay.io/centos/centos:stream9
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || IMAGE=${{ matrix.image }} make test-templates
e2e-tests:
name: End to end Tests
runs-on: ubuntu-latest
strategy:
matrix:
image:
- alpine:3.17
- ubuntu:20.04
- ubuntu:22.04
- debian:10
- debian:11
- centos:8
- quay.io/centos/centos:stream9
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system ovmf
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-e2e-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Run end-to-end tests
run: E2E_IMAGES=${{ matrix.image }} make e2e
docs-up-to-date:
name: Docs up to date
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Check if docs are up to date
run: make docs-up-to-date
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Ensure all files were well formatted
run: make check-fmt
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v4
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
with:
gpg_private_key: ${{ secrets.GPG_KEY }}
passphrase: ${{ secrets.GPG_PASSWORD }}
- name: Build Snapshot
run: make build-snapshot
- name: Release Snapshot
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
env:
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
run: make release-snapshot
build-image:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-build-image-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-image-
- name: Build Docker images
run: make docker-build
- name: Push Docker images
if: github.ref == 'refs/heads/main'
run: make docker-push
release:
name: Release Binaries
runs-on: ubuntu-latest
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- templates-tests
- docs-up-to-date
- build
- e2e-tests
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v4
with:
gpg_private_key: ${{ secrets.GPG_KEY }}
passphrase: ${{ secrets.GPG_PASSWORD }}
- name: Build binaries
run: make build
- name: Release binaries
env:
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
run: make release
release-image:
name: Release Docker Image
runs-on: ubuntu-latest
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- docs-up-to-date
- build-image
- e2e-tests
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-build-image-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-image-
- name: Build Docker images
run: make docker-build
- name: Release Docker images
run: make docker-push

View File

@ -1,20 +0,0 @@
name: Docs
on:
push:
tags: [ "v*" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and deploy mkdocs site
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
make build-docs
sudo chown -R ${UID}:${UID} docs
make deploy-docs

14
.gitignore vendored
View File

@ -1,20 +1,6 @@
.idea
tests
scratch
*.qcow2
*.vmdk
*.vdi
.DS_Store
bin/
dist/
images
/d2vm
/examples/build
/examples/full/demo-magic
/examples/full/inside
.goreleaser.yaml
docs/build
docs-src
/completions
/cmd/d2vm/run/sparsecat-linux-*

View File

@ -1,81 +0,0 @@
# Copyright 2022 Linka Cloud All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
project_name: d2vm
before:
hooks:
- go mod tidy
- go generate ./...
- make completions
builds:
- main: ./cmd/d2vm
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X "go.linka.cloud/d2vm.Image={{.Env.IMAGE}}" -X "go.linka.cloud/d2vm.Version={{.Env.VERSION}}" -X "go.linka.cloud/d2vm.BuildDate={{.CommitDate}}"
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
checksum:
name_template: 'checksums.txt'
signs:
- artifacts: all
stdin: '{{ .Env.GPG_PASSWORD }}'
snapshot:
name_template: "{{ .Env.VERSION }}"
release:
prerelease: auto
extra_files:
- glob: LICENCE
- glob: pgp.pub
archives:
- name_template: '{{ .ProjectName }}_{{ .Env.VERSION }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- LICENCE
- README.md
- completions/*
brews:
- name: d2vm
tap:
owner: linka-cloud
name: homebrew-tap
folder: Formula
homepage: https://github.com/linka-cloud/d2vm
description: Build Virtual Machine Image from Dockerfile or Docker image
license: Apache License 2.0
test: |
system "#{bin}/d2vm --version"
dependencies:
- name: go
type: optional
- name: git
install: |-
bin.install "d2vm"
bash_completion.install "completions/d2vm.bash" => "d2vm"
zsh_completion.install "completions/d2vm.zsh" => "_d2vm"
fish_completion.install "completions/d2vm.fish"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^tests:'
- '^actions:'
- '^Makefile:'
- '^chore:'
- '^goreleaser:'

View File

@ -1,18 +1,4 @@
# Copyright 2022 Linka Cloud All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.20 as builder
FROM golang as builder
WORKDIR /d2vm
@ -23,26 +9,21 @@ RUN go mod download
COPY . .
RUN make .build
RUN make build
FROM ubuntu:20.04
FROM ubuntu
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
ca-certificates \
util-linux \
udev \
parted \
kpartx \
e2fsprogs \
dosfstools \
xfsprogs \
mount \
tar \
"$([ "$(uname -m)" = "x86_64" ] && echo extlinux)" \
cryptsetup-bin \
qemu-utils && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
extlinux \
uuid-runtime \
qemu-utils
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/

121
Makefile
View File

@ -16,42 +16,20 @@ MODULE = go.linka.cloud/d2vm
REPOSITORY = linkacloud
TAG = $(shell git diff --quiet && git describe --tags --exact-match 2> /dev/null)
VERSION_SUFFIX = $(shell git diff --quiet || echo "-dev")
VERSION = $(shell git describe --tags --exact-match 2> /dev/null || echo "`git describe --tags $$(git rev-list --tags --max-count=1) 2> /dev/null || echo v0.0.0`-`git rev-parse --short HEAD`")$(VERSION_SUFFIX)
show-version:
@echo $(VERSION)
GORELEASER_VERSION := v1.10.1
GORELEASER_URL := https://github.com/goreleaser/goreleaser/releases/download/$(GORELEASER_VERSION)/goreleaser_Linux_x86_64.tar.gz
BIN := $(PWD)/bin
export PATH := $(BIN):$(PATH)
CLI_REFERENCE_PATH := docs/content/reference
bin:
@mkdir -p $(BIN)
@curl -sL $(GORELEASER_URL) | tar -C $(BIN) -xz goreleaser
clean-bin:
@rm -rf $(BIN)
DOCKER_IMAGE := linkacloud/d2vm
docker: docker-build docker-push
docker-push:
@docker image push $(DOCKER_IMAGE):$(VERSION)
ifneq ($(TAG),)
@docker image push $(DOCKER_IMAGE):latest
endif
@docker image push -a $(DOCKER_IMAGE)
docker-build:
@docker image build -t $(DOCKER_IMAGE):$(VERSION) .
ifneq ($(TAG),)
@docker image tag $(DOCKER_IMAGE):$(TAG) $(DOCKER_IMAGE):latest
endif
@docker image build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest .
docker-run:
@docker run --rm -i -t \
@ -61,96 +39,5 @@ docker-run:
-w /build \
$(DOCKER_IMAGE) bash
.PHONY: tests
tests:
@go generate ./...
@go list .| xargs go test -exec sudo -count=1 -timeout 60m -v -skip TestConfig
.PHONY: test-templates
test-templates:
@go generate ./...
@go test -exec sudo -count=1 -timeout 60m -v -run TestConfig/$(IMAGE)
e2e: docker-build .build
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e -args -images $(E2E_IMAGES)
docs-up-to-date:
@$(MAKE) cli-docs
@git diff --quiet -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md' || (git --no-pager diff -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md'; echo "Please regenerate the documentation with 'make cli-docs'"; exit 1)
check-fmt:
@[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
vet:
@go list ./...|grep -v scratch|GOOS=linux xargs go vet
build-dev: docker-build .build
install: docker-build
@go generate ./...
@go install -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
.build:
@go generate ./...
@CGO_ENABLED=0 go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
.PHONY: build-snapshot
build-snapshot: bin
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser build --snapshot --rm-dist --parallelism 8
.PHONY: release-snapshot
release-snapshot: bin
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser release --snapshot --rm-dist --skip-announce --skip-publish --parallelism 8
.PHONY: build
build: bin
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser build --rm-dist --parallelism 8
.PHONY: release
release: bin
@VERSION=$(VERSION) IMAGE=$(DOCKER_IMAGE) goreleaser release --rm-dist --parallelism 8
.PHONY: completions
completions: .build
@rm -rf completions
@mkdir -p completions
@for shell in bash zsh fish powershell; do \
./d2vm completion $$shell > completions/d2vm.$$shell; \
done
.PHONY: examples
examples: build-dev
@mkdir -p examples/build
@for f in $$(find examples -maxdepth 1 -type f -name '*Dockerfile'); do \
echo "Building $$f"; \
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
done
@echo "Building examples/full/Dockerfile"
@./d2vm build -o examples/build/full.qcow2 --build-arg=USER=adphi --build-arg=PASSWORD=adphi examples/full --force
cli-docs: .build
@rm -rf $(CLI_REFERENCE_PATH)
@./d2vm docs $(CLI_REFERENCE_PATH)
serve-docs:
@docker run --rm -i -t --user=$(UID) -p 8000:8000 -v $(PWD):/docs linkacloud/mkdocs-material serve -f /docs/docs/mkdocs.yml -a 0.0.0.0:8000
.PHONY: build-docs
build-docs: clean-docs cli-docs
@docker run --rm -v $(PWD):/docs linkacloud/mkdocs-material build -f /docs/docs/mkdocs.yml -d build
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
GITHUB_PAGES_BRANCH := gh-pages
deploy-docs:
@git branch -D gh-pages &> /dev/null || true
@git checkout -b $(GITHUB_PAGES_BRANCH)
@rm .gitignore && mv docs docs-src && mv docs-src/build docs && rm -rf docs-src
@git add . && git commit -m "build docs" && git push origin --force $(GITHUB_PAGES_BRANCH)
@git checkout $(GIT_BRANCH)
docs: cli-docs build-docs deploy-docs
clean-docs:
@rm -rf docs/build
build:
@go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm

241
README.md
View File

@ -1,10 +1,5 @@
# d2vm (Docker to Virtual Machine)
[![Language: Go](https://img.shields.io/badge/lang-Go-6ad7e5.svg?style=flat-square&logo=go)](https://golang.org/)
[![Go Reference](https://pkg.go.dev/badge/go.linka.cloud/d2vm.svg)](https://pkg.go.dev/go.linka.cloud/d2vm)
[![Chat](https://img.shields.io/badge/chat-matrix-blue.svg?style=flat-square&logo=matrix)](https://matrix.to/#/#d2vm:linka.cloud)
*Build virtual machine image from Docker images*
The project is heavily inspired by the [article](https://iximiuz.com/en/posts/from-docker-container-to-bootable-linux-disk-image/) and the work done by [iximiuz](https://github.com/iximiuz) on [docker-to-linux](https://github.com/iximiuz/docker-to-linux).
@ -13,142 +8,70 @@ Many thanks to him.
**Status**: *alpha*
[![asciicast](https://asciinema.org/a/520132.svg)](https://asciinema.org/a/520132)
## Supported Environments:
**Only building Linux Virtual Machine images is supported.**
**Only Linux is supported.**
Starting from v0.1.0, **d2vm** automatically run build and convert commands inside Docker when not running on linux
or when running without *root* privileges.
If you want to run it on **OSX** or **Windows** (the last one is totally untested) you can do it using Docker:
*Note: windows should be working, but is totally untested.*
```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"
```
## Supported VM Linux distributions:
Working and tested:
- [x] Ubuntu (18.04+)
Luks support is available only on Ubuntu 20.04+
- [x] Debian (stretch+)
Luks support is available only on Debian buster+
- [x] Ubuntu
- [x] Debian
- [x] Alpine
- [x] CentOS (8+)
Need fix:
- [ ] CentOS
Unsupported:
- [ ] RHEL
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
The program use 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.
## Prerequisites
### osx
- [Docker](https://docs.docker.com/get-docker/)
- [QEMU](https://www.qemu.org/download/#macos) (optional)
- [VirtualBox](https://www.virtualbox.org/wiki/Downloads) (optional)
### Linux
- [Docker](https://docs.docker.com/get-docker/)
- util-linux
- udev
- parted
- e2fsprogs
- dosfstools (when using fat32)
- mount
- tar
- extlinux (when using syslinux)
- qemu-utils
- cryptsetup (when using LUKS)
- [QEMU](https://www.qemu.org/download/#linux) (optional)
- [VirtualBox](https://www.virtualbox.org/wiki/Linux_Downloads) (optional)
Obviously, **Distroless** images are not supported.
## Getting started
### Install
#### With Docker
*Note: this will only work if both the source context (and Dockerfile) and the output directory are somewhere inside
the directory where you run the command.*
```bash
docker pull linkacloud/d2vm:latest
alias d2vm="docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --privileged -v \$PWD:/d2vm -w /d2vm linkacloud/d2vm:latest"
```
```bash
which d2vm
d2vm: aliased to docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --privileged -v $PWD:/d2vm -w /d2vm linkacloud/d2vm:latest
```
#### With Homebrew
```bash
brew install linka-cloud/tap/d2vm
```
#### From release
Download the latest release for your platform from the [release page](https://github.com/linka-cloud/d2vm/releases/latest).
Extract the tarball, then move the extracted *d2vm* binary to somewhere in your `$PATH` (`/usr/local/bin` for most users).
```bash
VERSION=$(git ls-remote --tags https://github.com/linka-cloud/d2vm |cut -d'/' -f 3|tail -n 1)
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$([ "$(uname -m)" = "x86_64" ] && echo "amd64" || echo "arm64")
curl -sL "https://github.com/linka-cloud/d2vm/releases/download/${VERSION}/d2vm_${VERSION}_${OS}_${ARCH}.tar.gz" | tar -xvz d2vm
sudo mv d2vm /usr/local/bin/
```
#### From source
Clone the git repository:
```bash
git clone https://github.com/linka-cloud/d2vm && cd d2vm
```
Install using the *make*, *docker* and the Go tool chain:
```bash
make install
```
The *d2vm* binary is installed in the `$GOBIN` directory.
Install using the Go tool chain:
```bash
go install ./cmd/d2vm
which d2vm
```
```
# Should be install in the $GOBIN directory
/go/bin/d2vm
```
### Generate shell completion
The *d2vm* program supports shell completion for *bash*, *zsh* and *fish*.
It can be enabled by running the following command:
Or use an alias to the **docker** image:
```bash
source <(d2vm completion $(basename $SHELL))
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
```
Or you can install the completion file in the shell completion directory by following the instructions:
```bash
d2vm completion $(basename $SHELL) --help
```
d2vm: aliased to docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm
```
### Converting an existing Docker Image to VM image:
```bash
d2vm convert --help
b2vm convert --help
```
```
Convert Docker image to vm image
@ -157,28 +80,14 @@ Usage:
d2vm convert [docker image] [flags]
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
--split-boot Split the boot partition from the root partition
-t, --tag string Container disk Docker image tag
Global Flags:
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
-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")
```
@ -188,27 +97,27 @@ Create an image based on the **ubuntu** official image:
sudo d2vm convert ubuntu -o ubuntu.qcow2 -p MyP4Ssw0rd
```
```
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
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
```
You can now run your ubuntu image using the created `ubuntu.qcow2` image with **qemu**:
```bash
d2vm run qemu ubuntu.qcow2
./qemu.sh ununtu.qcow2
```
```
SeaBIOS (version 1.13.0-1ubuntu1.1)
@ -280,7 +189,7 @@ applicable law.
root@localhost:~#
```
Type `poweroff` to shut down the vm.
Type `poweroff` to shutdown the vm.
### Building a VM Image from a Dockerfile
@ -296,10 +205,12 @@ 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 \
```
When building the vm image, *d2vm* will create a root password, so there is no need to configure it now.
Build the vm image:
The *build* command take most of its flags and arguments from the *docker build* command.
@ -315,30 +226,15 @@ Usage:
d2vm build [context directory] [flags]
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
-h, --help help for build
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
--split-boot Split the boot partition from the root partition
-t, --tag string Container disk Docker image tag
Global Flags:
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
--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")
```
@ -349,16 +245,9 @@ 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 ubuntu.vdi .
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -O vdi -o ubuntu.vdi .
```
### KubeVirt Container Disk Images
Using the `--tag` flag with the `build` and `convert` commands, you can create a
[Container Disk Image](https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk) for [KubeVirt](https://kubevirt.io/).
The `--push` flag will push the image to the registry.
### Complete example
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
@ -372,8 +261,4 @@ 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 ?
- [x] Use image layers to create *rootfs* instead of container ?
### Acknowledgments
The *run* commands are adapted from [linuxkit](https://github.com/docker/linuxkit).
- [ ] Use image layers to create *rootfs* instead of container ?

View File

@ -1,43 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
)
var bootloaderProviders = map[string]BootloaderProvider{}
func RegisterBootloaderProvider(provider BootloaderProvider) {
bootloaderProviders[provider.Name()] = provider
}
func BootloaderByName(name string) (BootloaderProvider, error) {
if p, ok := bootloaderProviders[name]; ok {
return p, nil
}
return nil, fmt.Errorf("bootloader provider %s not found", name)
}
type BootloaderProvider interface {
New(c Config, r OSRelease, arch string) (Bootloader, error)
Name() string
}
type Bootloader interface {
Validate(fs BootFS) error
Setup(ctx context.Context, dev, root, cmdline string) error
}

View File

@ -15,15 +15,16 @@
package d2vm
import (
"bytes"
"context"
"fmt"
"math"
"os"
exec2 "os/exec"
"path/filepath"
"strings"
"github.com/c2h5oh/datasize"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"go.uber.org/multierr"
@ -41,67 +42,70 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
`
perm os.FileMode = 0644
syslinuxCfgUbuntu = `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
`
syslinuxCfgDebian = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /vmlinuz
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
`
syslinuxCfgAlpine = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz-virt
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200
`
syslinuxCfgCentOS = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
`
)
var formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vhd", "vhdx", "vmdk"}
var (
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
type Builder interface {
Build(ctx context.Context) (err error)
Close() error
}
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
mbrPaths = []string{
// debian path
"/usr/lib/syslinux/mbr/mbr.bin",
// ubuntu path
"/usr/lib/EXTLINUX/mbr.bin",
// alpine path
"/usr/share/syslinux/mbr.bin",
// centos path
"/usr/share/syslinux/mbr.bin",
}
)
type builder struct {
osRelease OSRelease
config Config
bootloader Bootloader
osRelease OSRelease
src string
img *image
diskRaw string
diskOut string
format string
size uint64
size int64
mntPoint string
splitBoot bool
bootSize uint64
bootFS BootFS
mbrPath string
loDevice string
bootPart string
rootPart string
cryptPart string
cryptRoot string
mappedCryptRoot string
bootUUID string
rootUUID string
cryptUUID string
luksPassword string
cmdLineExtra string
arch string
loDevice string
loPart string
diskUUD string
}
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootFS BootFS, bootSize uint64, luksPassword string, bootLoader string, platform string) (Builder, error) {
var arch string
switch platform {
case "linux/amd64":
arch = "x86_64"
case "linux/arm64", "linux/aarch64":
arch = "arm64"
default:
return nil, fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
}
if luksPassword != "" {
if !splitBoot {
return nil, fmt.Errorf("luks encryption requires split boot")
}
if !osRelease.SupportsLUKS() {
return nil, fmt.Errorf("luks encryption not supported on %s %s", osRelease.ID, osRelease.VersionID)
}
func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, format string) (*builder, error) {
if err := checkDependencies(); err != nil {
return nil, err
}
f := strings.ToLower(format)
valid := false
@ -113,91 +117,41 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
if !valid {
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
}
if f == "vhd" {
f = "vpc"
}
if splitBoot && bootSize < 50 {
return nil, fmt.Errorf("boot partition size must be at least 50MiB")
mbrBin := ""
for _, v := range mbrPaths {
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
}
}
if splitBoot && bootSize >= size {
return nil, fmt.Errorf("boot partition size must be less than the disk size")
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
}
if bootLoader == "" {
bootLoader = "syslinux"
}
config, err := osRelease.Config()
if err != nil {
return nil, err
}
if splitBoot {
config.Kernel = strings.TrimPrefix(config.Kernel, "/boot")
config.Initrd = strings.TrimPrefix(config.Initrd, "/boot")
}
if bootFS == "" {
bootFS = BootFSExt4
}
if err := bootFS.Validate(); err != nil {
return nil, err
}
blp, err := BootloaderByName(bootLoader)
if err != nil {
return nil, err
}
bl, err := blp.New(config, osRelease, arch)
if err != nil {
return nil, err
}
if err := bl.Validate(bootFS); err != nil {
return nil, err
}
if size == 0 {
size = 10 * uint64(datasize.GB)
size = 10 * int64(datasize.GB)
}
if disk == "" {
disk = "disk0"
}
img, err := NewImage(ctx, imgTag, workdir)
i, err := os.Stat(src)
if err != nil {
return nil, err
}
// 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,
config: config,
bootloader: bl,
img: img,
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
diskOut: filepath.Join(workdir, disk+"."+format),
format: f,
size: size,
mntPoint: filepath.Join(workdir, "/mnt"),
cmdLineExtra: cmdLineExtra,
splitBoot: splitBoot,
bootSize: bootSize,
bootFS: bootFS,
luksPassword: luksPassword,
arch: arch,
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)
}
if err := b.checkDependencies(); err != nil {
return nil, err
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"),
}
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err
@ -233,12 +187,15 @@ func (b *builder) Build(ctx context.Context) (err error) {
if err = b.setupRootFS(ctx); err != nil {
return err
}
if err = b.installBootloader(ctx); err != nil {
if err = b.installKernel(ctx); err != nil {
return err
}
if err = b.unmountImg(ctx); err != nil {
return err
}
if err = b.setupMBR(ctx); err != nil {
return err
}
if err = b.convert2Img(ctx); err != nil {
return err
}
@ -260,22 +217,19 @@ func (b *builder) makeImg(ctx context.Context) error {
if err := block(b.diskRaw, b.size); err != nil {
return err
}
var args []string
if b.splitBoot {
args = []string{"-s", b.diskRaw,
"mklabel", "msdos", "mkpart", "primary", "1Mib", fmt.Sprintf("%dMib", b.bootSize),
"mkpart", "primary", fmt.Sprintf("%dMib", b.bootSize), "100%",
"set", "1", "boot", "on",
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
}
} else {
args = []string{"-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"}
}
if err := exec.Run(ctx, "parted", args...); 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())
}
return nil
}
@ -289,62 +243,12 @@ func (b *builder) mountImg(ctx context.Context) error {
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
return err
}
b.bootPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
b.rootPart = ifElse(b.splitBoot, fmt.Sprintf("/dev/mapper/%sp2", filepath.Base(b.loDevice)), b.bootPart)
if b.isLuksEnabled() {
logrus.Infof("encrypting root partition")
f, err := os.CreateTemp("", "key")
if err != nil {
return err
}
defer f.Close()
defer os.Remove(f.Name())
if _, err := f.WriteString(b.luksPassword); err != nil {
return err
}
// cryptsetup luksFormat --batch-mode --verify-passphrase --type luks2 $ROOT_DEVICE $KEY_FILE
if err := exec.Run(ctx, "cryptsetup", "luksFormat", "--batch-mode", "--type", "luks2", b.rootPart, f.Name()); err != nil {
return err
}
b.cryptRoot = fmt.Sprintf("d2vm-%s-root", uuid.New().String())
// cryptsetup open -d $KEY_FILE $ROOT_DEVICE $ROOT_LABEL
if err := exec.Run(ctx, "cryptsetup", "open", "--key-file", f.Name(), b.rootPart, b.cryptRoot); err != nil {
return err
}
b.cryptPart = b.rootPart
b.rootPart = "/dev/mapper/root"
b.mappedCryptRoot = filepath.Join("/dev/mapper", b.cryptRoot)
logrus.Infof("creating raw image file system")
if err := exec.Run(ctx, "mkfs.ext4", b.mappedCryptRoot); err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil {
return err
}
} else {
logrus.Infof("creating raw image file system")
if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil {
return err
}
}
if !b.splitBoot {
return nil
}
if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil {
b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
logrus.Infof("creating raw image file system")
if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil {
return err
}
if b.bootFS.IsFat() {
err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart)
} else {
err = exec.Run(ctx, "mkfs.ext4", b.bootPart)
}
if err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil {
if err := exec.Run(ctx, "mount", b.loPart, b.mntPoint); err != nil {
return err
}
return nil
@ -353,118 +257,99 @@ func (b *builder) mountImg(ctx context.Context) error {
func (b *builder) unmountImg(ctx context.Context) error {
logrus.Infof("unmounting raw image")
var merr error
if b.splitBoot {
merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")))
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
merr = multierr.Append(merr, err)
}
merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint))
if b.isLuksEnabled() {
merr = multierr.Append(merr, exec.Run(ctx, "cryptsetup", "close", b.mappedCryptRoot))
if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
return multierr.Combine(
merr,
exec.Run(ctx, "kpartx", "-d", b.loDevice),
exec.Run(ctx, "losetup", "-d", b.loDevice),
)
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
return merr
}
func (b *builder) copyRootFS(ctx context.Context) error {
logrus.Infof("copying rootfs to raw image")
if err := b.img.Flatten(ctx, b.mntPoint); err != nil {
if err := exec.Run(ctx, "tar", "-xvf", b.src, "-C", b.mntPoint); err != nil {
return err
}
return nil
}
func diskUUID(ctx context.Context, disk string) (string, error) {
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", disk)
if err != nil {
return "", err
}
return strings.TrimSuffix(o, "\n"), nil
}
func (b *builder) setupRootFS(ctx context.Context) (err error) {
func (b *builder) setupRootFS(ctx context.Context) error {
logrus.Infof("setting up rootfs")
b.rootUUID, err = diskUUID(ctx, ifElse(b.isLuksEnabled(), b.mappedCryptRoot, b.rootPart))
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", b.loPart)
if err != nil {
return err
}
var fstab string
if b.splitBoot {
b.bootUUID, err = diskUUID(ctx, b.bootPart)
if err != nil {
return err
}
if b.isLuksEnabled() {
b.cryptUUID, err = diskUUID(ctx, b.cryptPart)
if err != nil {
return err
}
}
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot %s errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID, b.bootFS.linux())
} else {
b.bootUUID = b.rootUUID
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID)
}
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
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 {
return err
}
if err := b.chWriteFileIfNotExist("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
if err := b.chWriteFile("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
if err := b.chWriteFile("/etc/hostname", "localhost", 0644); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
if err := b.chWriteFile("/etc/hosts", hosts, 0644); err != nil {
return err
}
// TODO(adphi): is it the righ fix ?
if err := os.RemoveAll("/usr/sbin/policy-rc.d"); err != nil {
if err := os.RemoveAll("/ur/sbin/policy-rc.d"); err != nil {
return err
}
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
return err
}
switch b.osRelease.ID {
case ReleaseAlpine:
by, err := os.ReadFile(b.chPath("/etc/inittab"))
if err != nil {
return err
}
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
return err
}
return nil
default:
if b.osRelease.ID != ReleaseAlpine {
return nil
}
by, err := os.ReadFile(b.chPath("/etc/inittab"))
if err != nil {
return err
}
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
if err := b.chWriteFile("/etc/inittab", string(by), 0644); err != nil {
return err
}
if err := b.chWriteFile("/etc/network/interfaces", "", 0644); err != nil {
return err
}
return nil
}
func (b *builder) cmdline(_ context.Context) string {
if !b.isLuksEnabled() {
return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra)
func (b *builder) installKernel(ctx context.Context) error {
logrus.Infof("installing linux kernel")
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:
return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra)
sysconfig = syslinuxCfgAlpine
case ReleaseCentOS:
return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra)
sysconfig = syslinuxCfgCentOS
default:
// for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts...
// see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions
// and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html
return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra)
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
}
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), 0644); err != nil {
return err
}
return nil
}
func (b *builder) installBootloader(ctx context.Context) error {
logrus.Infof("installing bootloader")
return b.bootloader.Setup(ctx, b.loDevice, b.mntPoint, b.cmdline(ctx))
func (b *builder) setupMBR(ctx context.Context) error {
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", b.mbrPath), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil {
return err
}
return nil
}
func (b *builder) convert2Img(ctx context.Context) error {
@ -476,41 +361,22 @@ 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) isLuksEnabled() bool {
return b.luksPassword != ""
}
func (b *builder) Close() error {
return b.img.Close()
}
func block(path string, size uint64) error {
func block(path string, size int64) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return f.Truncate(int64(size))
return f.Truncate(size)
}
func (b *builder) checkDependencies() error {
func checkDependencies() error {
var merr error
deps := []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "dd", "mkfs.ext4", "cryptsetup"}
if _, ok := b.bootloader.(*syslinux); ok {
deps = append(deps, "extlinux")
}
for _, v := range deps {
for _, v := range []string{"mount", "blkid", "tar", "kpartx", "losetup", "qemu-img", "extlinux", "dd", "mkfs", "fdisk"} {
if _, err := exec2.LookPath(v); err != nil {
merr = multierr.Append(merr, err)
}
@ -521,10 +387,3 @@ func (b *builder) checkDependencies() error {
func OutputFormats() []string {
return formats[:]
}
func ifElse(v bool, t string, f string) string {
if v {
return t
}
return f
}

View File

@ -15,10 +15,6 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/google/uuid"
@ -27,98 +23,34 @@ import (
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
var (
file = "Dockerfile"
tag = "d2vm-" + uuid.New().String()
tag = uuid.New().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" || !isRoot() {
ctxAbsPath, err := filepath.Abs(args[0])
if err != nil {
return err
}
if file == "" {
file = filepath.Join(args[0], "Dockerfile")
}
dockerFileAbsPath, err := filepath.Abs(file)
if err != nil {
return err
}
if !strings.HasPrefix(dockerFileAbsPath, ctxAbsPath) {
return fmt.Errorf("Dockerfile must be in the context directory path")
}
outputPath, err := filepath.Abs(output)
if err != nil {
return err
}
var (
in = ctxAbsPath
out = filepath.Dir(outputPath)
)
dargs := os.Args[2:]
for i, v := range dargs {
switch v {
case file:
rel, err := filepath.Rel(in, dockerFileAbsPath)
if err != nil {
return fmt.Errorf("failed to construct Dockerfile container paths: %w", err)
}
dargs[i] = filepath.Join("/in", rel)
case output:
dargs[i] = filepath.Join("/out", filepath.Base(output))
case args[0]:
dargs[i] = "/in"
}
}
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...)
}
if err := validateFlags(); err != nil {
return err
}
size, err := parseSize(size)
if err != nil {
return err
}
if file == "" {
file = filepath.Join(args[0], "Dockerfile")
if debug {
exec.Run = exec.RunStdout
}
logrus.Infof("building docker image from %s", file)
if err := docker.Build(cmd.Context(), pull, tag, file, args[0], platform, buildArgs...); err != nil {
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 {
return err
}
if err := d2vm.Convert(
cmd.Context(),
tag,
d2vm.WithSize(size),
d2vm.WithPassword(password),
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithBootLoader(bootloader),
d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize),
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
d2vm.WithLuksPassword(luksPassword),
d2vm.WithKeepCache(keepCache),
d2vm.WithPlatform(platform),
d2vm.WithPull(false),
); err != nil {
return err
}
if uid, ok := sudoUser(); ok {
if err := os.Chown(output, uid, uid); err != nil {
return err
}
}
return maybeMakeContainerDisk(cmd.Context())
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
},
}
)
@ -126,8 +58,13 @@ var (
func init() {
rootCmd.AddCommand(buildCmd)
buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile")
buildCmd.Flags().StringVarP(&file, "file", "f", "Dockerfile", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
buildCmd.Flags().AddFlagSet(buildFlags())
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(&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")
}

View File

@ -1,43 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
)
func maybeMakeContainerDisk(ctx context.Context) error {
if containerDiskTag == "" {
return nil
}
logrus.Infof("creating container disk image %s", containerDiskTag)
if err := d2vm.MakeContainerDisk(ctx, output, containerDiskTag, platform); err != nil {
return err
}
if !push {
return nil
}
logrus.Infof("pushing container disk image %s", containerDiskTag)
if err := docker.Push(ctx, containerDiskTag); err != nil {
return fmt.Errorf("failed to push container disk: %w", err)
}
return nil
}

View File

@ -15,9 +15,9 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/c2h5oh/datasize"
"github.com/sirupsen/logrus"
@ -25,95 +25,77 @@ import (
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
var (
pull = false
convertCmd = &cobra.Command{
Use: "convert [docker image]",
Short: "Convert Docker image to vm image",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if runtime.GOOS != "linux" || !isRoot() {
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...)
}
if err := validateFlags(); err != nil {
return err
img := args[0]
tag := "latest"
if parts := strings.Split(img, ":"); len(parts) > 1 {
img, tag = parts[0], parts[1]
}
size, err := parseSize(size)
if err != nil {
return err
}
img := args[0]
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
if !force {
return fmt.Errorf("%s already exists", output)
}
}
if debug {
exec.Run = exec.RunStdout
}
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
if !force {
return fmt.Errorf("%s already exists", output)
}
}
found := false
if !pull {
imgs, err := docker.ImageList(cmd.Context(), img)
o, _, err := docker.CmdOut(cmd.Context(), "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", img)
if err != nil {
return err
}
found = len(imgs) == 1 && imgs[0] == img
found = strings.TrimSuffix(o, "\n") == fmt.Sprintf("%s:%s", img, tag)
if found {
logrus.Infof("using local image %s", img)
logrus.Infof("using local image %s:%s", img, tag)
}
}
if pull || !found {
logrus.Infof("pulling image %s", img)
if err := docker.Pull(cmd.Context(), platform, img); err != nil {
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
return err
}
}
if err := d2vm.Convert(
cmd.Context(),
img,
d2vm.WithSize(size),
d2vm.WithPassword(password),
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithBootLoader(bootloader),
d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize),
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
d2vm.WithLuksPassword(luksPassword),
d2vm.WithKeepCache(keepCache),
d2vm.WithPlatform(platform),
d2vm.WithPull(pull),
); err != nil {
return err
}
// set user permissions on the output file if the command was run with sudo
if uid, ok := sudoUser(); ok {
if err := os.Chown(output, uid, uid); err != nil {
return err
}
}
return maybeMakeContainerDisk(cmd.Context())
return d2vm.Convert(cmd.Context(), img, size, password, output, format)
},
}
)
func parseSize(s string) (uint64, error) {
func parseSize(s string) (int64, error) {
var v datasize.ByteSize
if err := v.UnmarshalText([]byte(s)); err != nil {
return 0, err
}
return uint64(v), nil
return int64(v), nil
}
func init() {
convertCmd.Flags().AddFlagSet(buildFlags())
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(&password, "password", "p", "root", "The Root user password")
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image")
rootCmd.AddCommand(convertCmd)
}

View File

@ -1,43 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var docsCmd = &cobra.Command{
Use: "docs",
Short: "Generate documentation",
Args: cobra.ExactArgs(1),
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
if err := os.MkdirAll(args[0], 0755); err != nil {
logrus.Fatal(err)
}
cmd.Root().DisableAutoGenTag = true
if err := doc.GenMarkdownTree(cmd.Root(), args[0]); err != nil {
logrus.Fatal(err)
}
},
}
func init() {
rootCmd.AddCommand(docsCmd)
}

View File

@ -1,120 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"os"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"go.linka.cloud/d2vm"
)
var (
output = "disk0.qcow2"
size = "1G"
password = ""
force = false
raw bool
pull = false
cmdLineExtra = ""
containerDiskTag = ""
push bool
networkManager string
bootloader string
splitBoot bool
bootSize uint64
bootFS string
luksPassword string
keepCache bool
platform string
)
func validateFlags() error {
switch platform {
case "linux/amd64":
if bootloader == "" {
bootloader = "syslinux"
}
case "linux/arm64", "linux/aarch64":
platform = "linux/arm64"
if bootloader == "" {
bootloader = "grub-efi"
}
if bootloader != "grub-efi" {
return fmt.Errorf("unsupported bootloader for platform %s: %s, only grub-efi is supported", platform, bootloader)
}
default:
return fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
}
if luksPassword != "" && !splitBoot {
logrus.Warnf("luks password is set: enabling split boot")
splitBoot = true
}
if bootFS := d2vm.BootFS(bootFS); bootFS != "" && !bootFS.IsSupported() {
return fmt.Errorf("invalid boot filesystem: %s", bootFS)
}
if bootFS != "" && !splitBoot {
logrus.Warnf("boot filesystem is set: enabling split boot")
splitBoot = true
}
efi := bootloader == "grub-efi" || bootloader == "grub"
if efi && !splitBoot {
logrus.Warnf("grub-efi bootloader is set: enabling split boot")
splitBoot = true
}
if efi && bootFS != "" && bootFS != "fat32" {
return fmt.Errorf("grub-efi bootloader only supports fat32 boot filesystem")
}
if efi && bootFS == "" {
logrus.Warnf("grub-efi bootloader is set: enabling fat32 boot filesystem")
bootFS = "fat32"
}
if push && tag == "" {
return fmt.Errorf("tag is required when pushing container disk image")
}
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
if !force {
return fmt.Errorf("%s already exists", output)
}
}
return nil
}
func buildFlags() *pflag.FlagSet {
flags := pflag.NewFlagSet("build", pflag.ExitOnError)
flags.StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
flags.StringVarP(&password, "password", "p", "", "Optional root user password")
flags.StringVarP(&size, "size", "s", "10G", "The output image size")
flags.BoolVar(&force, "force", false, "Override output qcow2 image")
flags.StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
flags.StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
flags.BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
flags.StringVarP(&containerDiskTag, "tag", "t", "", "Container disk Docker image tag")
flags.BoolVar(&push, "push", false, "Push the container disk image to the registry")
flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition")
flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB")
flags.StringVar(&bootFS, "boot-fs", "", "Filesystem to use for the boot partition, ext4 or fat32")
flags.StringVar(&bootloader, "bootloader", "", "Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64")
flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted")
flags.BoolVar(&keepCache, "keep-cache", false, "Keep the images after the build")
flags.StringVar(&platform, "platform", d2vm.Arch, "Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported")
flags.BoolVar(&pull, "pull", false, "Always pull docker image")
return flags
}

View File

@ -15,52 +15,25 @@
package main
import (
"bytes"
"context"
"fmt"
"os"
"os/signal"
"runtime"
"strconv"
"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 (
verbose = false
timeFormat = ""
format = "qcow2"
output = "disk0.qcow2"
size = "1G"
password = "root"
force = false
debug = false
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)
// make the zsh completion work when sourced with `source <(d2vm completion zsh)`
if cmd.Name() == "zsh" && cmd.Parent() != nil && cmd.Parent().Name() == "completion" {
zshHead := fmt.Sprintf("#compdef %[1]s\ncompdef _%[1]s %[1]s\n", cmd.Root().Name())
cmd.OutOrStdout().Write([]byte(zshHead))
}
},
}
)
@ -68,90 +41,5 @@ 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()
}()
if err := rootCmd.ExecuteContext(ctx); err != nil {
logrus.Fatal(err)
}
}
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "debug", "d", false, "Enable Debug output")
rootCmd.PersistentFlags().MarkDeprecated("debug", "use -v instead")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable Verbose output")
rootCmd.PersistentFlags().StringVar(&timeFormat, "time", "none", "Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)'")
color.NoColor = false
logrus.StandardLogger().Formatter = &logfmtFormatter{start: time.Now()}
}
const (
red = 31
yellow = 33
blue = 36
white = 39
gray = 90
)
type logfmtFormatter struct {
start time.Time
}
func (f *logfmtFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var b bytes.Buffer
var c *color.Color
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
c = color.New(gray)
case logrus.WarnLevel:
c = color.New(yellow)
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
c = color.New(red)
default:
c = color.New(white)
}
msg := entry.Message
if len(entry.Message) > 0 && entry.Level < logrus.DebugLevel {
msg = strings.ToTitle(string(msg[0])) + msg[1:]
}
var err error
switch timeFormat {
case "full", "f":
_, err = c.Fprintf(&b, "[%s] %s\n", entry.Time.Format("2006-01-02 15:04:05"), entry.Message)
case "relative", "rel", "r":
_, err = c.Fprintf(&b, "[%5v] %s\n", entry.Time.Sub(f.start).Truncate(time.Second).String(), msg)
default:
_, err = c.Fprintln(&b, msg)
}
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
func isRoot() bool {
return os.Geteuid() == 0
}
func sudoUser() (uid int, sudo bool) {
// if we are not running on linux, docker handle files user's permissions,
// so we don't need to check for sudo here
if runtime.GOOS != "linux" {
return
}
v := os.Getenv("SUDO_UID")
if v == "" {
return 0, false
}
uid, err := strconv.Atoi(v)
if err != nil {
logrus.Errorf("invalid SUDO_UID: %s", v)
return 0, false
}
return uid, true
rootCmd.ExecuteContext(ctx)
}

View File

@ -1,36 +0,0 @@
// 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)
}

View File

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

View File

@ -1,387 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package run
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/pkg/sftp"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/svenwiltink/sparsecat"
"go.linka.cloud/d2vm/pkg/qemu_img"
)
const (
hetznerTokenEnv = "HETZNER_TOKEN"
serverImg = "ubuntu-20.04"
vmBlock = "sda"
vmBlockPath = "/dev/" + vmBlock
sparsecatPath = "/usr/local/bin/sparsecat"
)
var (
hetznerVMType = "cx11"
hetznerToken = ""
// ash-dc1 fsn1-dc14 hel1-dc2 nbg1-dc3
hetznerDatacenter = "hel1-dc2"
hetznerServerName = "d2vm"
hetznerSSHUser = ""
hetznerSSHKeyPath = ""
hetznerRemove = false
HetznerCmd = &cobra.Command{
Use: "hetzner [options] image-path",
Short: "Run the virtual machine image on Hetzner Cloud",
Args: cobra.ExactArgs(1),
Run: Hetzner,
}
)
func init() {
HetznerCmd.Flags().StringVar(&hetznerToken, "token", "", "Hetzner Cloud API token [$"+hetznerTokenEnv+"]")
HetznerCmd.Flags().StringVarP(&hetznerSSHUser, "user", "u", "root", "d2vm image ssh user")
HetznerCmd.Flags().StringVarP(&hetznerSSHKeyPath, "ssh-key", "i", "", "d2vm image identity key")
HetznerCmd.Flags().BoolVar(&hetznerRemove, "rm", false, "remove server when done")
HetznerCmd.Flags().StringVarP(&hetznerServerName, "name", "n", "d2vm", "d2vm server name")
HetznerCmd.Flags().StringVarP(&hetznerVMType, "type", "t", hetznerVMType, "d2vm server type")
HetznerCmd.Flags().StringVarP(&hetznerDatacenter, "location", "l", hetznerDatacenter, "d2vm server location")
}
func Hetzner(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
if err := runHetzner(ctx, args[0], cmd.InOrStdin(), cmd.ErrOrStderr(), cmd.OutOrStdout()); err != nil {
logrus.Fatal(err)
}
}
func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.Writer, stdout io.Writer) error {
i, err := qemu_img.Info(ctx, imgPath)
if err != nil {
return err
}
if i.Format != "raw" {
logrus.Warnf("image format is %s, expected raw", i.Format)
rawPath := filepath.Join(os.TempDir(), "d2vm", "run", filepath.Base(imgPath)+".raw")
if err := os.MkdirAll(filepath.Dir(rawPath), 0755); err != nil {
return err
}
defer os.RemoveAll(rawPath)
logrus.Infof("converting image to raw: %s", rawPath)
if err := qemu_img.Convert(ctx, "raw", imgPath, rawPath); err != nil {
return err
}
imgPath = rawPath
i, err = qemu_img.Info(ctx, imgPath)
if err != nil {
return err
}
}
src, err := os.Open(imgPath)
if err != nil {
return err
}
defer src.Close()
c := hcloud.NewClient(hcloud.WithToken(GetStringValue(hetznerTokenEnv, hetznerToken, "")))
st, _, err := c.ServerType.GetByName(ctx, hetznerVMType)
if err != nil {
return err
}
arch := "amd64"
harch := hcloud.ArchitectureX86
if strings.HasPrefix(strings.ToLower(hetznerVMType), "cax") {
harch = hcloud.ArchitectureARM
arch = "arm64"
}
sparsecatBin, err := Sparsecat(arch)
if err != nil {
return err
}
imgs, _, err := c.Image.List(ctx, hcloud.ImageListOpts{Name: serverImg, Architecture: []hcloud.Architecture{harch}})
if err != nil {
return err
}
if len(imgs) == 0 {
return fmt.Errorf("no image found with name %s", serverImg)
}
l, _, err := c.Location.Get(ctx, hetznerDatacenter)
if err != nil {
return err
}
logrus.Infof("creating server %s", hetznerServerName)
sres, _, err := c.Server.Create(ctx, hcloud.ServerCreateOpts{
Name: hetznerServerName,
ServerType: st,
Image: imgs[0],
Location: l,
StartAfterCreate: hcloud.Ptr(false),
})
if err != nil {
return err
}
remove := true
defer func() {
if !remove && !hetznerRemove {
return
}
logrus.Infof("removing server %s", hetznerServerName)
// we use context.Background() here because we don't want the request to fail if the context has been cancelled
if _, err := c.Server.Delete(context.Background(), sres.Server); err != nil {
logrus.Fatalf("failed to remove server: %v", err)
}
}()
_, errs := c.Action.WatchProgress(ctx, sres.Action)
if err := <-errs; err != nil {
return err
}
logrus.Infof("server created with ip: %s", sres.Server.PublicNet.IPv4.IP.String())
logrus.Infof("enabling server rescue mode")
rres, _, err := c.Server.EnableRescue(ctx, sres.Server, hcloud.ServerEnableRescueOpts{Type: hcloud.ServerRescueTypeLinux64})
if err != nil {
return err
}
_, errs = c.Action.WatchProgress(ctx, rres.Action)
if err := <-errs; err != nil {
return err
}
logrus.Infof("powering on server")
pres, _, err := c.Server.Poweron(ctx, sres.Server)
if err != nil {
return err
}
_, errs = c.Action.WatchProgress(ctx, pres)
if err := <-errs; err != nil {
return err
}
logrus.Infof("connecting to server via ssh")
sc, err := dialSSHWithTimeout(sres.Server.PublicNet.IPv4.IP.String(), "root", rres.RootPassword, time.Minute)
if err != nil {
return err
}
defer sc.Close()
logrus.Infof("connection established")
sftpc, err := sftp.NewClient(sc)
if err != nil {
return err
}
f, err := sftpc.Create(sparsecatPath)
logrus.Debugf("creating sparsecat on remote host")
if err != nil {
return err
}
if err := sftpc.Chmod(sparsecatPath, 0755); err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, bytes.NewReader(sparsecatBin)); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
serrs := make(chan error, 2)
go func() {
serrs <- func() error {
s, err := sc.NewSession()
if err != nil {
return err
}
defer s.Close()
logrus.Infof("installing cloud-guest-utils on rescue server")
cmd := "apt update && apt install -y cloud-guest-utils"
logrus.Debugf("$ %s", cmd)
if b, err := s.CombinedOutput(cmd); err != nil {
return fmt.Errorf("%v: %s", err, string(b))
}
return nil
}()
}()
go func() {
serrs <- func() error {
wses, err := sc.NewSession()
if err != nil {
return err
}
defer wses.Close()
logrus.Infof("writing image to %s", vmBlockPath)
done := make(chan struct{})
defer close(done)
var r io.Reader
if runtime.GOOS == "linux" {
r = sparsecat.NewEncoder(src)
} else {
r = src
}
pr := newProgressReader(r)
wses.Stdin = pr
go func() {
tk := time.NewTicker(time.Second)
last := 0
for {
select {
case <-tk.C:
b := pr.Progress()
logrus.Infof("%s / %d%% transfered (%s/s)", humanize.Bytes(uint64(b)), int(float64(b)/float64(i.VirtualSize)*100), humanize.Bytes(uint64(b-last)))
last = b
case <-ctx.Done():
logrus.Warnf("context cancelled")
return
case <-done:
logrus.Infof("transfer finished")
return
}
}
}()
var cmd string
if runtime.GOOS == "linux" {
cmd = fmt.Sprintf("%s -r -disable-sparse-target -of %s", sparsecatPath, vmBlockPath)
} else {
cmd = fmt.Sprintf("dd of=%s", vmBlockPath)
}
logrus.Debugf("$ %s", cmd)
if b, err := wses.CombinedOutput(cmd); err != nil {
return fmt.Errorf("%v: %s", err, string(b))
} else {
logrus.Debugf(string(b))
}
return nil
}()
}()
for i := 0; i < 2; i++ {
select {
case err := <-serrs:
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
}
nses, err := sc.NewSession()
if err != nil {
return err
}
defer nses.Close()
// retrieve the partition number
cmd := fmt.Sprintf("ls %s*", vmBlockPath)
logrus.Debugf("$ %s", cmd)
b, err := nses.CombinedOutput(cmd)
if err != nil {
return fmt.Errorf("%v: %s", err, string(b))
}
logrus.Debugf(string(b))
parts := strings.Fields(strings.TrimSuffix(string(b), "\n"))
vmPartNumber, err := strconv.Atoi(strings.Replace(parts[len(parts)-1], vmBlockPath, "", 1))
if err != nil {
return err
}
gses, err := sc.NewSession()
if err != nil {
return err
}
defer gses.Close()
logrus.Infof("resizing disk partition")
cmd = fmt.Sprintf("growpart %s %d", vmBlockPath, vmPartNumber)
logrus.Debugf("$ %s", cmd)
if b, err := gses.CombinedOutput(cmd); err != nil {
return fmt.Errorf("%v: %s", err, string(b))
} else {
logrus.Debugf(string(b))
}
cses, err := sc.NewSession()
if err != nil {
return err
}
defer cses.Close()
logrus.Infof("checking disk partition")
cmd = fmt.Sprintf("e2fsck -yf %s%d", vmBlockPath, vmPartNumber)
logrus.Debugf("$ %s", cmd)
if b, err := cses.CombinedOutput(cmd); err != nil {
return fmt.Errorf("%v: %s", err, string(b))
} else {
logrus.Debugf(string(b))
}
eses, err := sc.NewSession()
if err != nil {
return err
}
defer eses.Close()
logrus.Infof("extending partition file system")
cmd = fmt.Sprintf("resize2fs %s%d", vmBlockPath, vmPartNumber)
logrus.Debugf("$ %s", cmd)
if b, err := eses.CombinedOutput(cmd); err != nil {
return fmt.Errorf("%v: %s", err, string(b))
}
logrus.Infof("rebooting server")
rbres, _, err := c.Server.Reboot(ctx, sres.Server)
if err != nil {
return err
}
_, errs = c.Action.WatchProgress(ctx, rbres)
if err := <-errs; err != nil {
return err
}
remove = false
logrus.Infof("waiting for server to be ready")
t := time.NewTimer(time.Minute)
wait:
for {
select {
case <-t.C:
return fmt.Errorf("ssh connection timeout")
case <-ctx.Done():
return ctx.Err()
default:
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:22", sres.Server.PublicNet.IPv4.IP.String()))
if err == nil {
conn.Close()
break wait
}
time.Sleep(time.Second)
}
}
logrus.Infof("server ready")
args := []string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}
if hetznerSSHKeyPath != "" {
args = append(args, "-i", hetznerSSHKeyPath)
}
args = append(args, fmt.Sprintf("%s@%s", hetznerSSHUser, sres.Server.PublicNet.IPv4.IP.String()))
logrus.Debugf("$ ssh %s", strings.Join(args, " "))
sshCmd := exec.CommandContext(ctx, "ssh", args...)
sshCmd.Stdin = stdin
sshCmd.Stderr = stderr
sshCmd.Stdout = stdout
if err := sshCmd.Run(); err != nil {
return err
}
return nil
}

View File

@ -1,127 +0,0 @@
package run
import (
"os"
"runtime"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.linka.cloud/d2vm/pkg/qemu"
)
const (
qemuNetworkingNone string = "none"
qemuNetworkingUser = "user"
qemuNetworkingTap = "tap"
qemuNetworkingBridge = "bridge"
qemuNetworkingDefault = qemuNetworkingUser
)
var (
defaultArch string
defaultAccel string
enableGUI bool
disks Disks
data string
accel string
arch string
cpus uint
mem uint
bios string
qemuCmd string
qemuDetached bool
networking string
publishFlags MultipleFlag
QemuCmd = &cobra.Command{
Use: "qemu [options] [image-path]",
Short: "Run the virtual machine image with qemu",
Args: cobra.ExactArgs(1),
Run: Qemu,
}
)
func init() {
switch runtime.GOARCH {
case "arm64":
defaultArch = "aarch64"
case "amd64":
defaultArch = "x86_64"
case "s390x":
defaultArch = "s390x"
}
switch {
case runtime.GOARCH == "s390x":
defaultAccel = "kvm"
case haveKVM():
defaultAccel = "kvm:tcg"
case runtime.GOOS == "darwin":
defaultAccel = "hvf:tcg"
}
flags := QemuCmd.Flags()
flags.BoolVar(&enableGUI, "gui", false, "Set qemu to use video output instead of stdio")
// Paths and settings for disks
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
// VM configuration
flags.StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
flags.StringVar(&arch, "arch", defaultArch, "Type of architecture to use, e.g. x86_64, aarch64, s390x")
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
flags.StringVar(&bios, "bios", "", "Path to the optional bios binary")
// Backend configuration
flags.StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
flags.BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background")
// Networking
flags.StringVar(&networking, "networking", qemuNetworkingDefault, "Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.`")
flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])")
}
func Qemu(cmd *cobra.Command, args []string) {
path := args[0]
if _, err := os.Stat(path); err != nil {
log.Fatal(err)
}
var publishedPorts []PublishedPort
for _, publish := range publishFlags {
p, err := NewPublishedPort(publish)
if err != nil {
log.Fatal(err)
}
publishedPorts = append(publishedPorts, p)
}
opts := []qemu.Option{
qemu.WithDisks(disks...),
qemu.WithAccel(accel),
qemu.WithArch(arch),
qemu.WithCPUs(cpus),
qemu.WithMemory(mem),
qemu.WithNetworking(networking),
qemu.WithStdin(os.Stdin),
qemu.WithStdout(os.Stdout),
qemu.WithStderr(os.Stderr),
qemu.WithBios(bios),
}
if enableGUI {
opts = append(opts, qemu.WithGUI())
}
if qemuDetached {
opts = append(opts, qemu.WithDetached())
}
if err := qemu.Run(cmd.Context(), path, opts...); err != nil {
log.Fatal(err)
}
}
func haveKVM() bool {
_, err := os.Stat("/dev/kvm")
return !os.IsNotExist(err)
}

View File

@ -1,349 +0,0 @@
//go:generate env GOOS=linux GOARCH=amd64 go build -o sparsecat-linux-amd64 github.com/svenwiltink/sparsecat/cmd/sparsecat
//go:generate env GOOS=linux GOARCH=arm64 go build -o sparsecat-linux-arm64 github.com/svenwiltink/sparsecat/cmd/sparsecat
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package run
import (
"bufio"
_ "embed"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
"go.linka.cloud/d2vm/pkg/qemu"
)
//go:embed sparsecat-linux-amd64
var sparsecatAmdBinary []byte
//go:embed sparsecat-linux-arm64
var sparsecatArmBinary []byte
func Sparsecat(arch string) ([]byte, error) {
switch arch {
case "amd64":
return sparsecatAmdBinary, nil
case "arm64":
return sparsecatArmBinary, nil
default:
return nil, fmt.Errorf("unsupported architecture: %s", arch)
}
}
// Handle flags with multiple occurrences
type MultipleFlag []string
func (f *MultipleFlag) String() string {
return "A multiple flag is a type of flag that can be repeated any number of times"
}
func (f *MultipleFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
func (f *MultipleFlag) Type() string {
return "multiple-flag"
}
func GetStringValue(envKey string, flagVal string, defaultVal string) string {
var res string
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
res = os.Getenv(envKey)
}
// If a flag is specified, this value takes precedence
// Ignore cases where the flag carries the default value
if flagVal != "" && flagVal != defaultVal {
res = flagVal
}
// if we still don't have a value, use the default
if res == "" {
res = defaultVal
}
return res
}
func GetIntValue(envKey string, flagVal int, defaultVal int) int {
var res int
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
var err error
res, err = strconv.Atoi(os.Getenv(envKey))
if err != nil {
res = 0
}
}
// If a flag is specified, this value takes precedence
// Ignore cases where the flag carries the default value
if flagVal > 0 {
res = flagVal
}
// if we still don't have a value, use the default
if res == 0 {
res = defaultVal
}
return res
}
func GetBoolValue(envKey string, flagVal bool) bool {
var res bool
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
switch os.Getenv(envKey) {
case "":
res = false
case "0":
res = false
case "false":
res = false
case "FALSE":
res = false
case "1":
res = true
default:
// catches "true", "TRUE" or anything else
res = true
}
}
// If a flag is specified, this value takes precedence
if res != flagVal {
res = flagVal
}
return res
}
func StringToIntArray(l string, sep string) ([]int, error) {
var err error
if l == "" {
return []int{}, err
}
s := strings.Split(l, sep)
i := make([]int, len(s))
for idx := range s {
if i[idx], err = strconv.Atoi(s[idx]); err != nil {
return nil, err
}
}
return i, nil
}
// Convert a multi-line string into an array of strings
func SplitLines(in string) []string {
res := []string{}
s := bufio.NewScanner(strings.NewReader(in))
for s.Scan() {
res = append(res, s.Text())
}
return res
}
// This function parses the "size" parameter of a disk specification
// and returns the size in MB. The "size" parameter defaults to GB, but
// the unit can be explicitly set with either a G (for GB) or M (for
// MB). It returns the disk size in MB.
func GetDiskSizeMB(s string) (int, error) {
if s == "" {
return 0, nil
}
sz := len(s)
if strings.HasSuffix(s, "M") {
return strconv.Atoi(s[:sz-1])
}
if strings.HasSuffix(s, "G") {
s = s[:sz-1]
}
i, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return 1024 * i, nil
}
func ConvertMBtoGB(i int) int {
if i < 1024 {
return 1
}
if i%1024 == 0 {
return i / 1024
}
return (i + (1024 - i%1024)) / 1024
}
// Disks is the type for a list of DiskConfig
type Disks []qemu.Disk
func (l *Disks) String() string {
return fmt.Sprint(*l)
}
// Set is used by flag to configure value from CLI
func (l *Disks) Set(value string) error {
d := qemu.Disk{}
s := strings.Split(value, ",")
for _, p := range s {
c := strings.SplitN(p, "=", 2)
switch len(c) {
case 1:
// assume it is a filename even if no file=x
d.Path = c[0]
case 2:
switch c[0] {
case "file":
d.Path = c[1]
case "size":
size, err := GetDiskSizeMB(c[1])
if err != nil {
return err
}
d.Size = size
case "format":
d.Format = c[1]
default:
return fmt.Errorf("Unknown disk config: %s", c[0])
}
}
}
*l = append(*l, d)
return nil
}
func (l *Disks) Type() string {
return "disk"
}
// PublishedPort is used by some backends to expose a VMs port on the host
type PublishedPort struct {
Guest uint16
Host uint16
Protocol string
}
// NewPublishedPort parses a string of the form <host>:<guest>[/<tcp|udp>] and returns a PublishedPort structure
func NewPublishedPort(publish string) (PublishedPort, error) {
p := PublishedPort{}
slice := strings.Split(publish, ":")
if len(slice) < 2 {
return p, fmt.Errorf("Unable to parse the ports to be published, should be in format <host>:<guest> or <host>:<guest>/<tcp|udp>")
}
hostPort, err := strconv.ParseUint(slice[0], 10, 16)
if err != nil {
return p, fmt.Errorf("The provided hostPort can't be converted to uint16")
}
right := strings.Split(slice[1], "/")
protocol := "tcp"
if len(right) == 2 {
protocol = strings.TrimSpace(strings.ToLower(right[1]))
}
if protocol != "tcp" && protocol != "udp" {
return p, fmt.Errorf("Provided protocol is not valid, valid options are: udp and tcp")
}
guestPort, err := strconv.ParseUint(right[0], 10, 16)
if err != nil {
return p, fmt.Errorf("The provided guestPort can't be converted to uint16")
}
if hostPort < 1 || hostPort > 65535 {
return p, fmt.Errorf("Invalid hostPort: %d", hostPort)
}
if guestPort < 1 || guestPort > 65535 {
return p, fmt.Errorf("Invalid guestPort: %d", guestPort)
}
p.Guest = uint16(guestPort)
p.Host = uint16(hostPort)
p.Protocol = protocol
return p, nil
}
func dialSSH(server, user, password string) (*ssh.Client, error) {
c, err := ssh.Dial("tcp", server+":22", &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
return nil, err
}
return c, nil
}
func dialSSHWithTimeout(server, user, password string, timeout time.Duration) (*ssh.Client, error) {
t := time.NewTimer(timeout)
for {
select {
case <-t.C:
return nil, fmt.Errorf("timeout while trying to connect to the server")
default:
c, err := dialSSH(server, user, password)
if err == nil {
return c, nil
}
time.Sleep(time.Second)
}
}
}
func newProgressReader(r io.Reader) *pw {
return &pw{r: r}
}
type pw struct {
r io.Reader
total int
size int
mu sync.RWMutex
}
func (p *pw) Read(buf []byte) (int, error) {
p.mu.Lock()
p.total += len(buf)
p.mu.Unlock()
return p.r.Read(buf)
}
func (p *pw) Progress() int {
p.mu.RLock()
defer p.mu.RUnlock()
return p.total
}

View File

@ -1,351 +0,0 @@
package run
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.linka.cloud/console"
"go.linka.cloud/d2vm/pkg/qemu_img"
)
var (
VboxCmd = &cobra.Command{
Use: "vbox [options] image-path",
Short: "Run the virtual machine image with Virtualbox",
Args: cobra.ExactArgs(1),
Run: Vbox,
}
vboxmanageFlag string
name string
networks VBNetworks
)
func init() {
flags := VboxCmd.Flags()
// Display flags
flags.Bool("gui", false, "Show the VM GUI")
// vbox options
flags.StringVar(&vboxmanageFlag, "vboxmanage", "VBoxManage", "VBoxManage binary to use")
flags.StringVar(&name, "name", "d2vm", "Name of the Virtualbox VM")
// Paths and settings for disks
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=raw]")
// VM configuration
flags.Uint("cpus", 1, "Number of CPUs")
flags.Uint("mem", 1024, "Amount of memory in MB")
// networking
flags.Var(&networks, "networking", "Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>]")
if runtime.GOOS == "windows" {
log.Fatalf("TODO: Windows is not yet supported")
}
}
func Vbox(cmd *cobra.Command, args []string) {
path := args[0]
if err := vbox(cmd.Context(), path); err != nil {
logrus.Fatal(err)
}
}
func vbox(ctx context.Context, path string) error {
if _, err := os.Stat(path); err != nil {
return err
}
vboxmanage, err := exec.LookPath(vboxmanageFlag)
if err != nil {
return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err)
}
i, err := qemu_img.Info(ctx, path)
if err != nil {
return fmt.Errorf("failed to get image info: %v", err)
}
if i.Format != "vdi" {
logrus.Warnf("image format is %s, expected vdi", i.Format)
vdi := filepath.Join(os.TempDir(), "d2vm", "run", filepath.Base(path)+".vdi")
if err := os.MkdirAll(filepath.Dir(vdi), 0755); err != nil {
return err
}
defer os.RemoveAll(vdi)
logrus.Infof("converting image to raw: %s", vdi)
if err := qemu_img.Convert(ctx, "vdi", path, vdi); err != nil {
return err
}
path = vdi
}
// remove machine in case it already exists
cleanup(vboxmanage, name, false)
_, out, err := manage(vboxmanage, "createvm", "--name", name, "--register")
if err != nil {
return fmt.Errorf("createvm error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--acpi", "on")
if err != nil {
return fmt.Errorf("modifyvm --acpi error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--memory", fmt.Sprintf("%d", mem))
if err != nil {
return fmt.Errorf("modifyvm --memory error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--cpus", fmt.Sprintf("%d", cpus))
if err != nil {
return fmt.Errorf("modifyvm --cpus error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--firmware", "bios")
if err != nil {
return fmt.Errorf("modifyvm --firmware error: %v\n%s", err, out)
}
// set up serial console
_, out, err = manage(vboxmanage, "modifyvm", name, "--uart1", "0x3F8", "4")
if err != nil {
return fmt.Errorf("modifyvm --uart error: %v\n%s", err, out)
}
consolePath := filepath.Join(os.TempDir(), "d2vm-vb", name, "console")
if err := os.MkdirAll(filepath.Dir(consolePath), os.ModePerm); err != nil {
return fmt.Errorf("mkir %s: %v", consolePath, err)
}
if runtime.GOOS != "windows" {
consolePath, err = filepath.Abs(consolePath)
if err != nil {
return fmt.Errorf("Bad path: %v", err)
}
} else {
// TODO use a named pipe on Windows
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--uartmode1", "client", consolePath)
if err != nil {
return fmt.Errorf("modifyvm --uartmode error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "IDE Controller", "--add", "ide")
if err != nil {
return fmt.Errorf("storagectl error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", path)
if err != nil {
return fmt.Errorf("storageattach error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "disk")
if err != nil {
return fmt.Errorf("modifyvm --boot error: %v\n%s", err, out)
}
if len(disks) > 0 {
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "SATA", "--add", "sata")
if err != nil {
return fmt.Errorf("storagectl error: %v\n%s", err, out)
}
}
for i, d := range disks {
id := strconv.Itoa(i)
if d.Size != 0 && d.Format == "" {
d.Format = "raw"
}
if d.Format != "raw" && d.Path == "" {
return fmt.Errorf("vbox currently can only create raw disks")
}
if d.Path == "" && d.Size == 0 {
return fmt.Errorf("please specify an existing disk file or a size")
}
if d.Path == "" {
d.Path = "disk" + id + ".img"
if err := os.Truncate(d.Path, int64(d.Size)*int64(1048576)); err != nil {
return fmt.Errorf("Cannot create disk: %v", err)
}
}
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", d.Path)
if err != nil {
return fmt.Errorf("storageattach error: %v\n%s", err, out)
}
}
for i, d := range networks {
nic := i + 1
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nictype%d", nic), "virtio")
if err != nil {
return fmt.Errorf("modifyvm --nictype error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nic%d", nic), d.Type)
if err != nil {
return fmt.Errorf("modifyvm --nic error: %v\n%s", err, out)
}
if d.Type == "hostonly" {
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--hostonlyadapter%d", nic), d.Adapter)
if err != nil {
return fmt.Errorf("modifyvm --hostonlyadapter error: %v\n%s", err, out)
}
} else if d.Type == "bridged" {
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--bridgeadapter%d", nic), d.Adapter)
if err != nil {
return fmt.Errorf("modifyvm --bridgeadapter error: %v\n%s", err, out)
}
}
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--cableconnected%d", nic), "on")
if err != nil {
return fmt.Errorf("modifyvm --cableconnected error: %v\n%s", err, out)
}
}
// create socket
_ = os.Remove(consolePath)
ln, err := net.Listen("unix", consolePath)
if err != nil {
return fmt.Errorf("Cannot listen on console socket %s: %v", consolePath, err)
}
defer ln.Close()
var vmType string
if enableGUI {
vmType = "gui"
} else {
vmType = "headless"
}
term := console.Current()
ws, err := term.Size()
if err != nil {
return fmt.Errorf("get term size: %v", err)
}
_, out, err = manage(vboxmanage, "startvm", name, "--type", vmType)
if err != nil {
return fmt.Errorf("startvm error: %v\n%s", err, out)
}
defer cleanup(vboxmanage, name)
if err := term.Resize(ws); err != nil && !errors.Is(err, console.ErrUnsupported) {
return fmt.Errorf("resize term: %v", err)
}
if err := term.SetRaw(); err != nil {
return fmt.Errorf("set raw term: %v", err)
}
defer func() {
if err := term.Reset(); err != nil {
log.Errorf("failed to reset term: %v", err)
}
}()
socket, err := ln.Accept()
if err != nil {
return fmt.Errorf("Accept error: %v", err)
}
defer socket.Close()
errs := make(chan error, 2)
go func() {
_, err := io.Copy(socket, term)
errs <- err
}()
go func() {
_, err := io.Copy(term, socket)
errs <- err
}()
return <-errs
}
func cleanup(vboxmanage string, name string, logErrs ...bool) {
logErr := true
if len(logErrs) > 0 {
logErr = logErrs[0]
}
if _, _, err := manage(vboxmanage, "controlvm", name, "poweroff"); err != nil && logErr {
log.Errorf("controlvm poweroff error: %v", err)
}
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", "emptydrive")
if err != nil && logErr {
log.Errorf("storageattach error: %v\n%s", err, out)
}
for i := range disks {
id := strconv.Itoa(i)
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", "emptydrive")
if err != nil && logErr {
log.Errorf("storageattach error: %v\n%s", err, out)
}
}
if _, out, err = manage(vboxmanage, "unregistervm", name, "--delete"); err != nil && logErr {
log.Errorf("unregistervm error: %v\n%s", err, out)
}
}
func manage(vboxmanage string, args ...string) (string, string, error) {
log.Debugf("$ %s %s", vboxmanage, strings.Join(args, " "))
cmd := exec.Command(vboxmanage, args...)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = io.MultiWriter(&stdout, logrus.StandardLogger().WriterLevel(logrus.DebugLevel))
cmd.Stderr = io.MultiWriter(&stderr, logrus.StandardLogger().WriterLevel(logrus.DebugLevel))
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
// VBNetwork is the config for a Virtual Box network
type VBNetwork struct {
Type string
Adapter string
}
// VBNetworks is the type for a list of VBNetwork
type VBNetworks []VBNetwork
func (l *VBNetworks) String() string {
return fmt.Sprint(*l)
}
func (l *VBNetworks) Type() string {
return "vbnetworks"
}
// Set is used by flag to configure value from CLI
func (l *VBNetworks) Set(value string) error {
d := VBNetwork{}
s := strings.Split(value, ",")
for _, p := range s {
c := strings.SplitN(p, "=", 2)
switch len(c) {
case 1:
d.Type = c[0]
case 2:
switch c[0] {
case "type":
d.Type = c[1]
case "adapter", "bridgeadapter", "hostadapter":
d.Adapter = c[1]
default:
return fmt.Errorf("Unknown network config: %s", c[0])
}
}
}
*l = append(*l, d)
return nil
}

View File

@ -1,17 +1,3 @@
// 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 (

View File

@ -1,89 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"fmt"
"strings"
)
var (
configUbuntu = Config{
Kernel: "/boot/vmlinuz",
Initrd: "/boot/initrd.img",
}
configDebian = Config{
Kernel: "/boot/vmlinuz",
Initrd: "/boot/initrd.img",
}
configAlpine = Config{
Kernel: "/boot/vmlinuz-virt",
Initrd: "/boot/initramfs-virt",
}
configCentOS = Config{
Kernel: "/boot/vmlinuz",
Initrd: "/boot/initrd.img",
}
)
type Root interface {
String() string
}
type RootUUID string
func (r RootUUID) String() string {
return "UUID=" + string(r)
}
type RootPath string
func (r RootPath) String() string {
return string(r)
}
type Config struct {
Kernel string
Initrd string
}
func (c Config) Cmdline(root Root, args ...string) string {
var r string
if root != nil {
r = fmt.Sprintf("root=%s", root.String())
}
return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", c.Initrd, r, strings.Join(args, " "))
}
func (r OSRelease) Config() (Config, error) {
switch r.ID {
case ReleaseUbuntu:
if r.VersionID < "20.04" {
return configDebian, nil
}
return configUbuntu, nil
case ReleaseDebian:
return configDebian, nil
case ReleaseKali:
return configDebian, nil
case ReleaseAlpine:
return configAlpine, nil
case ReleaseCentOS:
return configCentOS, nil
default:
return Config{}, fmt.Errorf("%s: distribution not supported", r.ID)
}
}

View File

@ -1,161 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
func testConfig(t *testing.T, ctx context.Context, name, img string, config Config, luks, grubBIOS, grubEFI bool) {
require.NoError(t, docker.Pull(ctx, Arch, img))
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(name))
require.NoError(t, os.MkdirAll(tmpPath, 0755))
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img)
require.NoError(t, err)
defer docker.Remove(ctx, img)
if !r.SupportsLUKS() && luks {
t.Skipf("LUKS not supported for %s", r.Version)
}
d, err := NewDockerfile(r, img, "root", "", luks, grubBIOS, grubEFI)
require.NoError(t, err)
logrus.Infof("docker image based on %s", d.Release.Name)
p := filepath.Join(tmpPath, docker.FormatImgName(name))
dir := filepath.Dir(p)
f, err := os.Create(p)
require.NoError(t, err)
defer f.Close()
require.NoError(t, d.Render(f))
imgUUID := uuid.New().String()
logrus.Infof("building kernel enabled image")
require.NoError(t, docker.Build(ctx, false, imgUUID, p, dir, Arch))
defer docker.Remove(ctx, imgUUID)
// we don't need to test the kernel location if grub is enabled
if grubBIOS || grubEFI {
return
}
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel))
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd))
}
func TestConfig(t *testing.T) {
t.Parallel()
tests := []struct {
image string
config Config
}{
{
image: "ubuntu:18.04",
config: configDebian,
},
{
image: "ubuntu:20.04",
config: configUbuntu,
},
{
image: "ubuntu:22.04",
config: configUbuntu,
},
{
image: "ubuntu:latest",
config: configUbuntu,
},
{
image: "debian:9",
config: configDebian,
},
{
image: "debian:10",
config: configDebian,
},
{
image: "debian:11",
config: configDebian,
},
{
image: "debian:latest",
config: configDebian,
},
{
image: "kalilinux/kali-rolling:latest",
config: configDebian,
},
{
image: "alpine:3.16",
config: configAlpine,
},
{
image: "alpine",
config: configAlpine,
},
{
image: "centos:8",
config: configCentOS,
},
{
image: "centos:latest",
config: configCentOS,
},
{
image: "quay.io/centos/centos:stream9",
config: configCentOS,
},
}
exec.SetDebug(true)
names := []string{"luks", "grub-bios", "grub-efi"}
bools := []bool{false, true}
for _, test := range tests {
test := test
t.Run(test.image, func(t *testing.T) {
t.Parallel()
for _, luks := range bools {
for _, grubBIOS := range bools {
for _, grubEFI := range bools {
luks := luks
grubBIOS := grubBIOS
grubEFI := grubEFI
n := []string{test.image}
for i, v := range []bool{luks, grubBIOS, grubEFI} {
if v {
n = append(n, names[i])
}
}
name := strings.Join(n, "-")
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testConfig(t, ctx, name, test.image, test.config, luks, grubBIOS, grubEFI)
})
}
}
}
})
}
}

View File

@ -1,67 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/qemu_img"
)
const (
// https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk-workflow-example
uid = 107
containerDiskDockerfile = `FROM scratch
ADD --chown=%[1]d:%[1]d %[2]s /disk/
`
)
func MakeContainerDisk(ctx context.Context, path string, tag string, platform string) error {
tmpPath := filepath.Join(os.TempDir(), "d2vm", uuid.New().String())
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tmpPath); err != nil {
logrus.Errorf("failed to remove tmp dir %s: %v", tmpPath, err)
}
}()
if _, err := os.Stat(path); err != nil {
return err
}
// convert may not be needed, but this will also copy the file in the tmp dir
qcow2 := filepath.Join(tmpPath, "disk.qcow2")
if err := qemu_img.Convert(ctx, "qcow2", path, qcow2); err != nil {
return err
}
disk := filepath.Base(qcow2)
dockerfileContent := fmt.Sprintf(containerDiskDockerfile, uid, disk)
dockerfile := filepath.Join(tmpPath, "Dockerfile")
if err := os.WriteFile(dockerfile, []byte(dockerfileContent), os.ModePerm); err != nil {
return fmt.Errorf("failed to write dockerfile: %w", err)
}
if err := docker.Build(ctx, false, tag, dockerfile, tmpPath, platform); err != nil {
return fmt.Errorf("failed to build container disk: %w", err)
}
return nil
}

View File

@ -17,22 +17,17 @@ 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, opts ...ConvertOption) error {
o := &convertOptions{}
for _, opt := range opts {
opt(o)
}
func Convert(ctx context.Context, img string, size int64, password string, output string, format string) error {
imgUUID := uuid.New().String()
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
@ -41,65 +36,55 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img)
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
if err != nil {
return err
}
if o.luksPassword != "" && !r.SupportsLUKS() {
return fmt.Errorf("luks is not supported for %s %s", r.Name, r.Version)
d, err := NewDockerfile(r, img, password)
if err != nil {
return err
}
if !o.raw {
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.hasGrubBIOS(), o.hasGrubEFI())
if err != nil {
return err
}
logrus.Infof("docker image based on %s %s", d.Release.Name, d.Release.Version)
p := filepath.Join(tmpPath, docker.FormatImgName(img))
dir := filepath.Dir(p)
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
if err := d.Render(f); err != nil {
return err
}
logrus.Infof("building kernel enabled image")
if err := docker.Build(ctx, o.pull, imgUUID, p, dir, o.platform); err != nil {
return err
}
if !o.keepCache {
defer docker.Remove(ctx, imgUUID)
}
} else {
// for raw images, we just tag the image with the uuid
if err := docker.Tag(ctx, img, imgUUID); err != nil {
return err
}
if !o.keepCache {
defer docker.Remove(ctx, imgUUID)
}
logrus.Infof("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")
format := strings.TrimPrefix(filepath.Ext(o.output), ".")
if format == "" {
format = "raw"
}
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.luksPassword, o.bootLoader, o.platform)
b, err := NewBuilder(tmpPath, archivePath, "", size, r, format)
if err != nil {
return err
}
defer b.Close()
if err := b.Build(ctx); err != nil {
return err
}
if err := os.RemoveAll(o.output); err != nil {
if err := os.RemoveAll(output); err != nil {
return err
}
if err := MoveFile(filepath.Join(tmpPath, "disk0."+format), o.output); err != nil {
if err := MoveFile(filepath.Join(tmpPath, "disk0.qcow2"), output); err != nil {
return err
}
return nil
@ -116,7 +101,7 @@ func MoveFile(sourcePath, destPath string) error {
return fmt.Errorf("failed to open dest file: %s", err)
}
defer outputFile.Close()
_, err = sparsecat.NewDecoder(sparsecat.NewEncoder(inputFile)).WriteTo(outputFile)
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return fmt.Errorf("failed to write to output file: %s", err)

View File

@ -1,129 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
type ConvertOption func(o *convertOptions)
type convertOptions struct {
size uint64
password string
output string
cmdLineExtra string
networkManager NetworkManager
bootLoader string
raw bool
splitBoot bool
bootSize uint64
bootFS BootFS
luksPassword string
keepCache bool
platform string
pull bool
}
func (o *convertOptions) hasGrubBIOS() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-bios"
}
func (o *convertOptions) hasGrubEFI() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-efi"
}
func WithSize(size uint64) ConvertOption {
return func(o *convertOptions) {
o.size = size
}
}
func WithPassword(password string) ConvertOption {
return func(o *convertOptions) {
o.password = password
}
}
func WithOutput(output string) ConvertOption {
return func(o *convertOptions) {
o.output = output
}
}
func WithCmdLineExtra(cmdLineExtra string) ConvertOption {
return func(o *convertOptions) {
o.cmdLineExtra = cmdLineExtra
}
}
func WithNetworkManager(networkManager NetworkManager) ConvertOption {
return func(o *convertOptions) {
o.networkManager = networkManager
}
}
func WithBootLoader(bootLoader string) ConvertOption {
return func(o *convertOptions) {
o.bootLoader = bootLoader
}
}
func WithRaw(raw bool) ConvertOption {
return func(o *convertOptions) {
o.raw = raw
}
}
func WithSplitBoot(b bool) ConvertOption {
return func(o *convertOptions) {
o.splitBoot = b
}
}
func WithBootSize(bootSize uint64) ConvertOption {
return func(o *convertOptions) {
o.bootSize = bootSize
}
}
func WithBootFS(bootFS BootFS) ConvertOption {
return func(o *convertOptions) {
o.bootFS = bootFS
}
}
func WithLuksPassword(password string) ConvertOption {
return func(o *convertOptions) {
o.luksPassword = password
}
}
func WithKeepCache(b bool) ConvertOption {
return func(o *convertOptions) {
o.keepCache = b
}
}
func WithPlatform(platform string) ConvertOption {
return func(o *convertOptions) {
o.platform = platform
}
}
func WithPull(b bool) ConvertOption {
return func(o *convertOptions) {
o.pull = b
}
}

View File

@ -15,122 +15,58 @@
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/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
const (
dockerImageRun = `
#!/bin/sh
{{- range .DockerImageConfig.Env }}
{{- range .Config.Env }}
export {{ . }}
{{- end }}
{{ if .DockerImageConfig.WorkingDir }}cd {{ .DockerImageConfig.WorkingDir }}{{ end }}
cd {{- if .Config.WorkingDir }}{{ .Config.WorkingDir }}{{- else }}/{{- end }}
{{ 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 }}
{{ .Config.Entrypoint }} {{ .Config.Args }}
`
)
var (
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
dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Parse(dockerImageRun))
)
type DockerImage struct {
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"`
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"`
}
func (i DockerImage) AsRunScript(w io.Writer) error {
return dockerImageRunTemplate.Execute(w, i)
}
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
if err := os.MkdirAll(imageTmpPath, os.ModePerm); err != nil {
return nil, err
}
// save the image to a tar file to avoid loading it in memory
tar := filepath.Join(imageTmpPath, "img.layers.tar")
if err := docker.ImageSave(ctx, tag, tar); err != nil {
return nil, err
}
img, err := crane.Load(tar)
if err != nil {
return nil, err
}
i := &image{
img: img,
dir: imageTmpPath,
}
return i, nil
}
type image struct {
tag string
img v1.Image
dir string
Config string `json:"Config"`
RepoTags []string `json:"RepoTags"`
Layers []string `json:"Layers"`
}
func (i image) Flatten(ctx context.Context, out string) error {
if err := os.MkdirAll(out, os.ModePerm); err != nil {
return err
}
tar := filepath.Join(i.dir, "img.tar")
f, err := os.Create(tar)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, mutate.Extract(i.img)); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := exec.Run(ctx, "tar", "xvf", tar, "-C", out); err != nil {
return err
}
return nil
}
func (i image) Close() error {
return os.RemoveAll(i.dir)
}

View File

@ -1,168 +0,0 @@
package d2vm
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec"
)
func TestDockerImageAsRunSript(t *testing.T) {
tests := []struct {
name string
image DockerImage
want string
}{
{
name: "nothing",
image: DockerImage{
DockerImageConfig: DockerImageConfig{
User: "",
WorkingDir: "",
Env: nil,
Entrypoint: nil,
Cmd: nil,
},
},
want: `
#!/bin/sh
`,
},
{
name: "tail -f /dev/null",
image: DockerImage{
DockerImageConfig: DockerImageConfig{
User: "root",
Cmd: []string{"tail", "-f", "/dev/null"},
},
},
want: `
#!/bin/sh
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
`,
},
{
name: "tail -f /dev/null inside home",
image: DockerImage{
DockerImageConfig: DockerImageConfig{
User: "root",
WorkingDir: "/root",
Cmd: []string{"tail", "-f", "/dev/null"},
},
},
want: `
#!/bin/sh
cd /root
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
`,
},
{
name: "subshell tail -f /dev/null",
image: DockerImage{
DockerImageConfig: DockerImageConfig{
User: "root",
Entrypoint: []string{"/bin/sh", "-c"},
Cmd: []string{"tail -f /dev/null"},
},
},
want: `
#!/bin/sh
su root -p -s /bin/sh -c '"/bin/sh" "-c" "tail -f /dev/null"'
`,
},
{
name: "www-data with env",
image: DockerImage{
DockerImageConfig: DockerImageConfig{
User: "www-data",
Cmd: []string{"tail", "-f", "/dev/null"},
Env: []string{"ENV=PROD", "DB=mysql://user:password@localhost"},
},
},
want: `
#!/bin/sh
export ENV=PROD
export DB=mysql://user:password@localhost
su www-data -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var w bytes.Buffer
require.NoError(t, tt.image.AsRunScript(&w))
assert.Equal(t, tt.want, w.String())
})
}
}
func TestImageFlatten(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
img = "d2vm-flatten-test"
dockerfile = `FROM alpine
COPY resolv.conf /etc/
COPY hostname /etc/
RUN rm -rf /etc/apk
`
)
exec.SetDebug(true)
tmp := filepath.Join(os.TempDir(), "d2vm-tests", "image-flatten")
require.NoError(t, os.MkdirAll(tmp, os.ModePerm))
defer os.RemoveAll(tmp)
require.NoError(t, os.WriteFile(filepath.Join(tmp, "hostname"), []byte("d2vm-flatten-test"), perm))
require.NoError(t, os.WriteFile(filepath.Join(tmp, "resolv.conf"), []byte("nameserver 8.8.8.8"), perm))
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
require.NoError(t, docker.Build(ctx, false, img, "", tmp, "linux/amd64"))
defer docker.Remove(ctx, img)
imgTmp := filepath.Join(tmp, "image")
i, err := NewImage(ctx, img, imgTmp)
require.NoError(t, err)
rootfs := filepath.Join(tmp, "rootfs")
require.NoError(t, i.Flatten(ctx, rootfs))
b, err := os.ReadFile(filepath.Join(rootfs, "etc", "resolv.conf"))
require.NoError(t, err)
assert.Equal(t, "nameserver 8.8.8.8", string(b))
b, err = os.ReadFile(filepath.Join(rootfs, "etc", "hostname"))
require.NoError(t, err)
assert.Equal(t, "d2vm-flatten-test", string(b))
_, err = os.Stat(filepath.Join(rootfs, "etc", "apk"))
assert.Error(t, err)
require.NoError(t, i.Close())
_, err = os.Stat(imgTmp)
assert.Error(t, err)
}

View File

@ -18,10 +18,7 @@ import (
_ "embed"
"fmt"
"io"
"strconv"
"text/template"
"github.com/sirupsen/logrus"
)
//go:embed templates/ubuntu.Dockerfile
@ -37,92 +34,39 @@ var alpineDockerfile string
var centOSDockerfile string
var (
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile))
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile))
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile))
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile))
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Parse(ubuntuDockerfile))
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Parse(debianDockerfile))
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Parse(alpineDockerfile))
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
NetworkManager NetworkManager
Luks bool
GrubBIOS bool
GrubEFI bool
tmpl *template.Template
}
func (d Dockerfile) Grub() bool {
return d.GrubBIOS || d.GrubEFI
Image string
Password string
Release OSRelease
tmpl *template.Template
}
func (d Dockerfile) Render(w io.Writer) error {
return d.tmpl.Execute(w, d)
}
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grubBIOS, grubEFI bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, GrubBIOS: grubBIOS, GrubEFI: grubEFI}
var net NetworkManager
func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error) {
if password == "" {
password = "root"
}
d := Dockerfile{Release: release, Image: img, Password: password}
switch release.ID {
case ReleaseDebian:
d.tmpl = debianDockerfileTemplate
net = NetworkManagerIfupdown2
case ReleaseKali:
d.tmpl = debianDockerfileTemplate
net = NetworkManagerIfupdown2
case ReleaseUbuntu:
d.tmpl = ubuntuDockerfileTemplate
if release.VersionID < "18.04" {
net = NetworkManagerIfupdown2
} else {
net = NetworkManagerNetplan
}
case ReleaseAlpine:
d.tmpl = alpineDockerfileTemplate
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
}
var tplFuncs = template.FuncMap{
"atoi": strconv.Atoi,
}

View File

@ -1 +0,0 @@
d2vm.linka.cloud

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

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

View File

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

View File

@ -1,20 +0,0 @@
## d2vm
### Options
```
-h, --help help for d2vm
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm build](d2vm_build.md) - Build a vm image from Dockerfile
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell
* [d2vm convert](d2vm_convert.md) - Convert Docker image to vm image
* [d2vm run](d2vm_run.md) - Run the virtual machine image
* [d2vm version](d2vm_version.md) -

View File

@ -1,44 +0,0 @@
## d2vm build
Build a vm image from Dockerfile
```
d2vm build [context directory] [flags]
```
### Options
```
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
-h, --help help for build
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
--split-boot Split the boot partition from the root partition
-t, --tag string Container disk Docker image tag
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm](d2vm.md) -

View File

@ -1,31 +0,0 @@
## d2vm completion
Generate the autocompletion script for the specified shell
### Synopsis
Generate the autocompletion script for d2vm for the specified shell.
See each sub-command's help for details on how to use the generated script.
### Options
```
-h, --help help for completion
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm](d2vm.md) -
* [d2vm completion bash](d2vm_completion_bash.md) - Generate the autocompletion script for bash
* [d2vm completion fish](d2vm_completion_fish.md) - Generate the autocompletion script for fish
* [d2vm completion powershell](d2vm_completion_powershell.md) - Generate the autocompletion script for powershell
* [d2vm completion zsh](d2vm_completion_zsh.md) - Generate the autocompletion script for zsh

View File

@ -1,50 +0,0 @@
## d2vm completion bash
Generate the autocompletion script for bash
### Synopsis
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(d2vm completion bash)
To load completions for every new session, execute once:
#### Linux:
d2vm completion bash > /etc/bash_completion.d/d2vm
#### macOS:
d2vm completion bash > $(brew --prefix)/etc/bash_completion.d/d2vm
You will need to start a new shell for this setup to take effect.
```
d2vm completion bash
```
### Options
```
-h, --help help for bash
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell

View File

@ -1,41 +0,0 @@
## d2vm completion fish
Generate the autocompletion script for fish
### Synopsis
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
d2vm completion fish | source
To load completions for every new session, execute once:
d2vm completion fish > ~/.config/fish/completions/d2vm.fish
You will need to start a new shell for this setup to take effect.
```
d2vm completion fish [flags]
```
### Options
```
-h, --help help for fish
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell

View File

@ -1,38 +0,0 @@
## d2vm completion powershell
Generate the autocompletion script for powershell
### Synopsis
Generate the autocompletion script for powershell.
To load completions in your current shell session:
d2vm completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
```
d2vm completion powershell [flags]
```
### Options
```
-h, --help help for powershell
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell

View File

@ -1,52 +0,0 @@
## d2vm completion zsh
Generate the autocompletion script for zsh
### Synopsis
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(d2vm completion zsh)
To load completions for every new session, execute once:
#### Linux:
d2vm completion zsh > "${fpath[1]}/_d2vm"
#### macOS:
d2vm completion zsh > $(brew --prefix)/share/zsh/site-functions/_d2vm
You will need to start a new shell for this setup to take effect.
```
d2vm completion zsh [flags]
```
### Options
```
-h, --help help for zsh
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm completion](d2vm_completion.md) - Generate the autocompletion script for the specified shell

View File

@ -1,42 +0,0 @@
## d2vm convert
Convert Docker image to vm image
```
d2vm convert [docker image] [flags]
```
### Options
```
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
--split-boot Split the boot partition from the root partition
-t, --tag string Container disk Docker image tag
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm](d2vm.md) -

View File

@ -1,24 +0,0 @@
## d2vm run
Run the virtual machine image
### Options
```
-h, --help help for run
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm](d2vm.md) -
* [d2vm run hetzner](d2vm_run_hetzner.md) - Run the virtual machine image on Hetzner Cloud
* [d2vm run qemu](d2vm_run_qemu.md) - Run the virtual machine image with qemu
* [d2vm run vbox](d2vm_run_vbox.md) - Run the virtual machine image with Virtualbox

View File

@ -1,32 +0,0 @@
## d2vm run hetzner
Run the virtual machine image on Hetzner Cloud
```
d2vm run hetzner [options] image-path [flags]
```
### Options
```
-h, --help help for hetzner
-l, --location string d2vm server location (default "hel1-dc2")
-n, --name string d2vm server name (default "d2vm")
--rm remove server when done
-i, --ssh-key string d2vm image identity key
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
-t, --type string d2vm server type (default "cx11")
-u, --user string d2vm image ssh user (default "root")
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm run](d2vm_run.md) - Run the virtual machine image

View File

@ -1,36 +0,0 @@
## d2vm run qemu
Run the virtual machine image with qemu
```
d2vm run qemu [options] [image-path] [flags]
```
### Options
```
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "kvm:tcg")
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
--bios string Path to the optional bios binary
--cpus uint Number of CPUs (default 1)
--detached Set qemu container to run in the background
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])
--gui Set qemu to use video output instead of stdio
-h, --help help for qemu
--mem uint Amount of memory in MB (default 1024)
--networking string Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.` (default "user")
--publish multiple-flag Publish a vm's port(s) to the host (default []) (default A multiple flag is a type of flag that can be repeated any number of times)
--qemu string Path to the qemu binary (otherwise look in $PATH)
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm run](d2vm_run.md) - Run the virtual machine image

View File

@ -1,32 +0,0 @@
## d2vm run vbox
Run the virtual machine image with Virtualbox
```
d2vm run vbox [options] image-path [flags]
```
### Options
```
--cpus uint Number of CPUs (default 1)
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=raw] (default [])
--gui Show the VM GUI
-h, --help help for vbox
--mem uint Amount of memory in MB (default 1024)
--name string Name of the Virtualbox VM (default "d2vm")
--networking vbnetworks Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>] (default [])
--vboxmanage string VBoxManage binary to use (default "VBoxManage")
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm run](d2vm_run.md) - Run the virtual machine image

View File

@ -1,25 +0,0 @@
## d2vm version
```
d2vm version [flags]
```
### Options
```
-h, --help help for version
```
### Options inherited from parent commands
```
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
-v, --verbose Enable Verbose output
```
### SEE ALSO
* [d2vm](d2vm.md) -

View File

@ -1,43 +0,0 @@
site_name: ""
docs_dir: content
site_dir: build
edit_uri: edit/docs/docs/content/
theme:
name: linka-cloud
logo: assets/d2vm-light-tr.png
favicon: assets/d2vm-favicon.png
language: en
repo_url: https://github.com/linka-cloud/d2vm
copyright: Copyright &copy; 2022 Linka Cloud
nav:
- Getting Started: index.md
- Complete Example: full-example.md
- Command Line:
- d2vm: reference/d2vm.md
- build: reference/d2vm_build.md
- convert: reference/d2vm_convert.md
- run:
- hetzner: reference/d2vm_run_hetzner.md
- qemu: reference/d2vm_run_qemu.md
- virtualbox: reference/d2vm_run_vbox.md
- completion:
- bash: reference/d2vm_completion_bash.md
- fish: reference/d2vm_completion_fish.md
- powershell: reference/d2vm_completion_powershell.md
- zsh: reference/d2vm_completion_zsh.md
- version: reference/d2vm_version.md
extra:
homepage: https://github.com/linka-cloud/d2vm
social:
- icon: fontawesome/brands/github
link: https://github.com/linka-cloud
- icon: fontawesome/brands/docker
link: https://hub.docker.com/r/linkacloud
markdown_extensions:
- pymdownx.highlight:
use_pygments: true
- pymdownx.superfences
- pymdownx.tasklist

View File

@ -1,212 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package e2e
import (
"bufio"
"bytes"
"context"
"flag"
"io"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
require2 "github.com/stretchr/testify/require"
"go.linka.cloud/d2vm"
"go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/qemu"
)
type test struct {
name string
args []string
efi bool
}
type img struct {
name string
luks string
}
var (
images = []img{
{name: "alpine:3.17", luks: "Enter passphrase for /dev/sda2:"},
{name: "ubuntu:20.04", luks: "Please unlock disk root:"},
{name: "ubuntu:22.04", luks: "Please unlock disk root:"},
{name: "debian:10", luks: "Please unlock disk root:"},
{name: "debian:11", luks: "Please unlock disk root:"},
{name: "centos:8", luks: "Please enter passphrase for disk"},
{name: "quay.io/centos/centos:stream9", luks: "Please enter passphrase for disk"},
}
imgNames = func() []string {
var imgs []string
for _, img := range images {
imgs = append(imgs, img.name)
}
return imgs
}()
imgs = flag.String("images", "", "comma separated list of images to test, must be one of: "+strings.Join(imgNames, ","))
)
func TestConvert(t *testing.T) {
require := require2.New(t)
tests := []test{
{
name: "single-partition",
},
{
name: "split-boot",
args: []string{"--split-boot"},
},
{
name: "fat32",
args: []string{"--split-boot", "--boot-fs=fat32"},
},
{
name: "luks",
args: []string{"--luks-password=root"},
},
{
name: "grub",
args: []string{"--bootloader=grub"},
efi: true,
},
{
name: "grub-luks",
args: []string{"--bootloader=grub", "--luks-password=root"},
efi: true,
},
}
var testImgs []img
imgs:
for _, v := range strings.Split(*imgs, ",") {
for _, img := range images {
if img.name == v {
testImgs = append(testImgs, img)
continue imgs
}
}
t.Fatalf("invalid image: %q, valid images: %s", v, strings.Join(imgNames, ","))
}
if len(testImgs) == 0 {
testImgs = images
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := filepath.Join("/tmp", "d2vm-e2e", tt.name)
require.NoError(os.MkdirAll(dir, os.ModePerm))
defer os.RemoveAll(dir)
for _, img := range testImgs {
if strings.Contains(img.name, "centos") && tt.efi {
t.Skip("efi not supported for CentOS")
}
t.Run(img.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
require := require2.New(t)
out := filepath.Join(dir, strings.NewReplacer(":", "-", ".", "-", "/", "-").Replace(img.name)+".qcow2")
if _, err := os.Stat(out); err == nil {
require.NoError(os.Remove(out))
}
require.NoError(docker.RunD2VM(ctx, d2vm.Image, d2vm.Version, dir, dir, "convert", append([]string{"-p", "root", "-o", "/out/" + filepath.Base(out), "-v", "--keep-cache", img.name}, tt.args...)...))
inr, inw := io.Pipe()
defer inr.Close()
outr, outw := io.Pipe()
defer outw.Close()
var success atomic.Bool
go func() {
time.AfterFunc(2*time.Minute, cancel)
defer inw.Close()
defer outr.Close()
login := []byte("login:")
password := []byte("Password:")
s := bufio.NewScanner(outr)
s.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if i := bytes.Index(data, []byte(img.luks)); i >= 0 {
return i + len(img.luks), []byte(img.luks), nil
}
if i := bytes.Index(data, login); i >= 0 {
return i + len(login), login, nil
}
if i := bytes.Index(data, password); i >= 0 {
return i + len(password), password, nil
}
if atEOF {
return 0, nil, io.EOF
}
return 0, nil, nil
})
for s.Scan() {
b := s.Bytes()
if bytes.Contains(b, []byte(img.luks)) {
t.Logf("sending luks password")
if _, err := inw.Write([]byte("root\n")); err != nil {
t.Logf("failed to write luks password: %v", err)
cancel()
}
}
if bytes.Contains(b, login) {
t.Logf("sending login")
if _, err := inw.Write([]byte("root\n")); err != nil {
t.Logf("failed to write login: %v", err)
cancel()
}
}
if bytes.Contains(b, password) {
t.Logf("sending password")
if _, err := inw.Write([]byte("root\n")); err != nil {
t.Logf("failed to write password: %v", err)
cancel()
}
time.Sleep(time.Second)
if _, err := inw.Write([]byte("poweroff\n")); err != nil {
t.Logf("failed to write poweroff: %v", err)
cancel()
}
success.Store(true)
return
}
}
if err := s.Err(); err != nil {
t.Logf("failed to scan output: %v", err)
cancel()
}
}()
opts := []qemu.Option{qemu.WithStdin(inr), qemu.WithStdout(io.MultiWriter(outw, os.Stdout)), qemu.WithStderr(io.Discard), qemu.WithMemory(2048), qemu.WithCPUs(2)}
if tt.efi {
opts = append(opts, qemu.WithBios("/usr/share/ovmf/OVMF.fd"))
}
if err := qemu.Run(ctx, out, opts...); err != nil && !success.Load() {
t.Fatalf("failed to run qemu: %v", err)
}
})
}
})
}
}

View File

@ -1,5 +1,4 @@
FROM alpine
RUN apk add --no-cache openrc openssh-server && \
rc-update add sshd default && \
RUN apk add --no-cache openssh-server && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config

View File

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

View File

@ -1,13 +1,15 @@
FROM ubuntu
# Install some system packages
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
# Install netplan sudo ssh-server and dns utils
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
qemu-guest-agent \
ca-certificates \
netplan.io \
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/
@ -17,17 +19,14 @@ ARG PASSWORD=d2vm
ARG SSH_KEY=https://github.com/${USER}.keys
# Setup user environment
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
bash-completion \
curl \
zsh \
git \
vim \
tmux \
htop \
lsb-core \
cloud-init \
cloud-guest-utils
htop
# Create user with sudo privileged and passwordless sudo
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
@ -46,5 +45,4 @@ USER ${USER}
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
# Setup tmux environment
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
# Setup auto login serial console
RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --keep-baud 115200,38400,9600 \%I \$TERM|" /usr/lib/systemd/system/serial-getty@.service

View File

@ -1,19 +1,21 @@
# ZSH Workstation example
# d2vm full example
This example demonstrate the setup of a ZSH workstation with *cloud-init* support.
This example demonstrate the setup of a ZSH workstation.
*Dockerfile*
```dockerfile
FROM ubuntu
# Install some system packages
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
# Install netplan sudo ssh-server and dns utils
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
qemu-guest-agent \
ca-certificates \
netplan.io \
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/
@ -23,17 +25,13 @@ ARG PASSWORD=d2vm
ARG SSH_KEY=https://github.com/${USER}.keys
# Setup user environment
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
bash-completion \
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
curl \
zsh \
git \
vim \
tmux \
htop \
lsb-core \
cloud-init \
cloud-guest-utils
htop
# Create user with sudo privileged and passwordless sudo
RUN useradd ${USER} -m -s /bin/zsh -G sudo && \
@ -52,12 +50,22 @@ 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
```
There is no need to configure the network as **d2vm** will generate a *netplan* configuration that use DHCP.
*00-netconf.yaml*
```yaml
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: true
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4
```
**Build**
@ -66,29 +74,22 @@ USER=mygithubuser
PASSWORD=mysecurepasswordthatIwillneverusebecauseIuseMostlySSHkeys
OUTPUT=workstation.qcow2
d2vm build -o $OUTPUT --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys --force -v .
d2vm build -o $OUTPUT --force --build-arg USER=$USER --build-arg PASSWORD=$PASSWORD --build-arg SSH_KEY=https://github.com/$USER.keys .
```
Run it:
Run it using *libvirt's virt-install*:
```bash
d2vm run qemu --mem 4096 --cpus 4 $IMAGE
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'
```
... you should be automatically logged in with a **oh-my-zsh** shell
You should be able to find the ip address inside the VM using:
From an other terminal you should be able to find the VM ip address using:
```bash
hostname -I
# or
ip a show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1
virsh domifaddr --domain workstation
```
And connect using ssh...
In order to quit the terminal you need to shut down the VM with the `poweroff` command:
```bash
sudo poweroff
```
*I hope you will find it useful and that you will have fun...*

View File

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

58
fs.go
View File

@ -1,58 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"fmt"
)
type BootFS string
const (
BootFSExt4 BootFS = "ext4"
BootFSFat32 BootFS = "fat32"
)
func (f BootFS) String() string {
return string(f)
}
func (f BootFS) IsExt() bool {
return f == BootFSExt4
}
func (f BootFS) IsFat() bool {
return f == BootFSFat32
}
func (f BootFS) IsSupported() bool {
return f.IsExt() || f.IsFat()
}
func (f BootFS) Validate() error {
if !f.IsSupported() {
fmt.Errorf("invalid boot filesystem: %s valid filesystems are: fat32, ext4", f)
}
return nil
}
func (f BootFS) linux() string {
switch f {
case BootFSFat32:
return "vfat"
default:
return "ext4"
}
}

70
go.mod
View File

@ -1,69 +1,19 @@
module go.linka.cloud/d2vm
go 1.20
go 1.17
require (
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0
github.com/google/go-containerregistry v0.14.0
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
github.com/google/uuid v1.3.0
github.com/hetznercloud/hcloud-go v1.50.0
github.com/joho/godotenv v1.5.1
github.com/pkg/sftp v1.10.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/svenwiltink/sparsecat v1.0.0
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.11.0
golang.org/x/sys v0.10.0
github.com/joho/godotenv v1.4.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
go.uber.org/multierr v1.8.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/creack/pty v1.1.15 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.4+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.4+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.11.1
golang.org/x/crypto => golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b
google.golang.org/protobuf => google.golang.org/protobuf v1.29.1
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
)

208
go.sum
View File

@ -1,205 +1,37 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 h1:t8KYCwSKsOEZBFELI4Pn/phbp38iJ1RRAkDFNin1aak=
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v23.0.4+incompatible h1:xClB7PsiATttDHj8ce5qvJcikiApNy7teRR1XkoBZGs=
github.com/docker/cli v23.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw=
github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hetznercloud/hcloud-go v1.50.0 h1:vS9tJvmSRwgDpMLmPnThGN87Rz8OMP3D4M3rWm8QHEQ=
github.com/hetznercloud/hcloud-go v1.50.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/svenwiltink/sparsecat v1.0.0 h1:SBDEIImxhD//8MskqodFR9VcixWKkZAPAW35nmA4vcw=
github.com/svenwiltink/sparsecat v1.0.0/go.mod h1:TdtvJbeTZcd+3cMQpttW6MJl/iPGZT0GHmckep0hoxU=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b h1:LccDAOuhRuyJqirU7I34UEEoYvuWWJ2jbs2Hyuc2aYU=
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b/go.mod h1:P/4MMfexPBBlmMpefLne7phPoNice1vhX65ZqOfzGII=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

76
grub.go
View File

@ -1,76 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
)
type grub struct {
*grubCommon
}
func (g grub) Validate(fs BootFS) error {
switch fs {
case BootFSFat32:
return nil
default:
return fmt.Errorf("grub only supports fat32 boot filesystem due to grub-efi")
}
}
func (g grub) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub bootloader")
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
defer clean()
if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
return err
}
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
return err
}
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubProvider struct {
config Config
}
func (g grubProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
if arch != "x86_64" {
return nil, fmt.Errorf("grub is only supported for amd64")
}
if r.ID == ReleaseCentOS {
return nil, fmt.Errorf("grub (efi) is not supported for CentOS, use grub-bios instead")
}
return grub{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubProvider) Name() string {
return "grub"
}
func init() {
RegisterBootloaderProvider(grubProvider{})
}

View File

@ -1,65 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
)
type grubBios struct {
*grubCommon
}
func (g grubBios) Validate(_ BootFS) error {
return nil
}
func (g grubBios) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub bootloader")
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
defer clean()
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
return err
}
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubBiosProvider struct {
config Config
}
func (g grubBiosProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
if arch != "x86_64" {
return nil, fmt.Errorf("grub-bios is only supported for amd64")
}
return grubBios{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubBiosProvider) Name() string {
return "grub-bios"
}
func init() {
RegisterBootloaderProvider(grubBiosProvider{})
}

View File

@ -1,102 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/exec"
)
const grubCfg = `GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=0
GRUB_CMDLINE_LINUX_DEFAULT="%s"
GRUB_CMDLINE_LINUX=""
GRUB_TERMINAL=console
`
type grubCommon struct {
name string
c Config
r OSRelease
root string
dev string
}
func newGrubCommon(c Config, r OSRelease) *grubCommon {
name := "grub"
if r.ID == "centos" {
name = "grub2"
}
return &grubCommon{
name: name,
c: c,
r: r,
}
}
func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (clean func(), err error) {
g.dev = dev
g.root = root
if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil {
return
}
if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil {
return
}
mounts := []string{"dev", "proc", "sys"}
var unmounts []string
clean = func() {
for _, v := range unmounts {
if err := exec.Run(ctx, "umount", filepath.Join(root, v)); err != nil {
logrus.Errorf("failed to unmount /%s: %s", v, err)
}
}
}
defer func() {
if err != nil {
clean()
}
}()
for _, v := range mounts {
if err = exec.Run(ctx, "mount", "-o", "bind", "/"+v, filepath.Join(root, v)); err != nil {
return
}
unmounts = append(unmounts, v)
}
return
}
func (g *grubCommon) install(ctx context.Context, args ...string) error {
if g.dev == "" || g.root == "" {
return fmt.Errorf("grubCommon not prepared")
}
args = append([]string{g.root, g.name + "-install"}, args...)
return exec.Run(ctx, "chroot", args...)
}
func (g *grubCommon) mkconfig(ctx context.Context) error {
if g.dev == "" || g.root == "" {
return fmt.Errorf("grubCommon not prepared")
}
return exec.Run(ctx, "chroot", g.root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg")
}

View File

@ -1,71 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
)
type grubEFI struct {
*grubCommon
arch string
}
func (g grubEFI) Validate(fs BootFS) error {
switch fs {
case BootFSFat32:
return nil
default:
return fmt.Errorf("grub-efi only supports fat32 boot filesystem")
}
}
func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub-efi bootloader")
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
defer clean()
if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
return err
}
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubEFIProvider struct {
config Config
}
func (g grubEFIProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) {
if r.ID == ReleaseCentOS {
return nil, fmt.Errorf("grub-efi is not supported for CentOS, use grub-bios instead")
}
return grubEFI{grubCommon: newGrubCommon(c, r), arch: arch}, nil
}
func (g grubEFIProvider) Name() string {
return "grub-efi"
}
func init() {
RegisterBootloaderProvider(grubEFIProvider{})
}

View File

@ -16,8 +16,11 @@ package d2vm
import (
"context"
"strconv"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
@ -31,7 +34,6 @@ const (
ReleaseAlpine Release = "alpine"
ReleaseCentOS Release = "centos"
ReleaseRHEL Release = "rhel"
ReleaseKali Release = "kali"
)
type Release string
@ -42,8 +44,6 @@ func (r Release) Supported() bool {
return true
case ReleaseDebian:
return true
case ReleaseKali:
return true
case ReleaseAlpine:
return true
case ReleaseCentOS:
@ -63,31 +63,6 @@ type OSRelease struct {
VersionCodeName string
}
func (r OSRelease) SupportsLUKS() bool {
switch r.ID {
case ReleaseUbuntu:
return r.VersionID >= "20.04"
case ReleaseDebian:
v, err := strconv.Atoi(r.VersionID)
if err != nil {
logrus.Warnf("%s: failed to parse version id: %v", r.Version, err)
return false
}
return v >= 10
case ReleaseKali:
// TODO: check version
return true
case ReleaseCentOS:
return true
case ReleaseAlpine:
return true
case ReleaseRHEL:
return false
default:
return false
}
}
func ParseOSRelease(s string) (OSRelease, error) {
env, err := godotenv.Parse(strings.NewReader(s))
if err != nil {
@ -103,8 +78,40 @@ func ParseOSRelease(s string) (OSRelease, error) {
return o, nil
}
func FetchDockerImageOSRelease(ctx context.Context, img string) (OSRelease, error) {
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", "--entrypoint", "cat", img, "/etc/os-release")
const (
osReleaseDockerfile = `
FROM {{ . }}
ENTRYPOINT [""]
CMD ["/bin/cat", "/etc/os-release"]
`
)
var (
osReleaseDockerfileTemplate = template.Must(template.New("osrelease.Dockerfile").Parse(osReleaseDockerfile))
)
func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) {
d := filepath.Join(tmpPath, "osrelase.Dockerfile")
f, err := os.Create(d)
if err != nil {
return OSRelease{}, err
}
defer f.Close()
if err := osReleaseDockerfileTemplate.Execute(f, img); err != nil {
return OSRelease{}, err
}
imgTag := fmt.Sprintf("os-release-%s", img)
if err := docker.Cmd(ctx, "image", "build", "-t", imgTag, "-f", d, tmpPath); err != nil {
return OSRelease{}, err
}
defer func() {
if err := docker.Cmd(ctx, "image", "rm", imgTag); err != nil {
logrus.WithError(err).Error("failed to cleanup OSRelease Docker Image")
}
}()
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", imgTag)
if err != nil {
return OSRelease{}, err
}

51
pgp.pub
View File

@ -1,51 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGLCuWcBEADzbuC8tyB0zPmReCu0Jwvc9tJqErtaYXxizM2XGiYc6iYOheFb
FnSFwlWK4TmtZ0XxIMMMJpIMVy1eEdbv3SBqHYXDWu+FzbEF32zfh/Sp4jzTbAZy
eKdXdcKYShhBNBnvfdQBsKG8J5PJi37w9yX9IVfviPymz1j42w7kvPMt6KM0HC3q
tJzwYLt6yizpY1DkT1lhypK2cgWrbBBTzxiUVHAK1Zxxr+MkSFowW5MHV0nfVWL2
5RU9mJny98YYvUJ7cQ1WaBjDQ9LSwMi+BVfZ87ABsc6NOyxPyEq7g/qwXdWL9QVq
bVFKvj2AaYeVJn17LYG3Ao8FfAt2tUN3FasrvmXnXumkqErwrCkGG5LFuRE11X/2
ntSf2Ra+dp81XHmugGHKdqdridQZPFTHeMQgk5Nyo0nNaQ0dO8w0zvLEqqQlEe4O
lTEK6MRPN2Pq9Rl2dC4YHU6ctnBEIwf98ysK2oIuHEnt8YZALg1jB2a1ANYOy25Z
UHOr2FlhCD6kdVpwGp99NH0lhKRcGl9BFDakYGWkjCqZT3UAZa+qKIp+wfNn75kI
1WjsIg0JFeH57RydlZf0eCelHY90rUKLB0kAlt/mWjoDyFnfrVHVfDh4yb/Gnwts
0cFWmink7fB0OLvzQvNGYPDkGG9tyjmepFm3OdD+wi2x3fPhPnB/o+sXXQARAQAB
tCJsaW5rYS1jbG91ZCA8c2VjdXJpdHlAbGlua2EtY2xvdWQ+iQJOBBMBCgA4FiEE
dRYaVRQmvhJpUDzmgEjKN+QesW0FAmLCuWcCGwMFCwkIBwIGFQoJCAsCBBYCAwEC
HgECF4AACgkQgEjKN+QesW0feQ/9H3HOjEv45em9JrdQu9UqjIG7VJG8En0fy2K2
oSQ65rOCRk1oxet2hay/jIkNfXwhSQ7pAOzs69JnDrXLYS9gZ3gSHRoWXSRe1way
mPIorq0hxaBJ6iJybmgOj4gqFmK+ObwcGUZq8y9hdD1UkQG9zM3+jPOh9Bd9t8ld
S8H4Sew1ZC2vVqh281BxvmJM9w94EuVw+4gJ72Rg74W2TOhK0qEvblHU7UbLI3s5
w+WvoLHQw3V6DmhuTsFWXWY1WXtlsYa5dE3QLWtXqCzt+yL1lw1opxgnP5BTBwjP
v5nZCWw6RC1EJxd2ac5IK8bXLcM0BtmHbVqA7cbeU4jc5TBj0Lu5o1AfUUCbyRMg
i7LDVn9ivCD3mymyGCtNsdj+oad9MwKJMlHNwzWNf2yE8GjxO09RYx/AzlubozCG
qlJvsEQUvUftGDW1adSdT+QRfIIS9pg2nZbMe+U0udrjpV1OGZDPgO7UNxi9u2Kl
JrVZ460K2psJEOBOaHg6Baj3HU1Ac6+UCigXbOx7WD7o3Rj8eNf1bIX/MXPfbBkv
Hh4dUchPuo1ImvU59w/jseuyXdiijCo5b5qeK2227XvXHIy3138x6gzs6JL5TXRp
F+D87+2WDu3yKjch0Sk0t+dKvuwsul/17wzDvMBIBMW96hRJhDVbUfaMPahf6Cfp
Gk3+t5W5Ag0EYsK5ZwEQAMJsn7eADPF9GshQRkzCcSxkeCh1OXWgPTOVEkBwKTeH
TXTEV4seANFHeUrTru3U/uuCsWi8eiwe5HTJKtPANud1iaUuMn7AyU0NteC8Bk9U
duwQXt02nke5jruNYOBm4j5yQYIBfa75ziDLUz7+NeAXIc3DhRM9gtE4N+5L95p+
bPPI077TldUkSLZM2kVIeWiAmZ1zsTg6SDW8wFMBoFfOtkEffZco4gzHlj8vPAc6
jkbgwrH7RZsWBcz2t7l/1AycDNPTElgFxnLxmBG01bNQuXTviNMZ0trNCtk2RdkS
iSLhIc3hMmaxnqy2mDzCVq/DCERo54iadXTJew9Y5jh2cO6V/rTJSnwaIh9AHMor
VO/kWPOzC/XMj1kL27DMwbByzm3619p0FgLPYqMdiWkmGhjVr0kcM+sBcepMliRs
eQy6q47IFm90XyUL9IXMtLWeleQ5/zHjD1CsKc/bSB5lzxLsv/IckYq8c9bbbPA0
bCGosQNQQMZTR/9sJ5VmYOWzDimqvX8l12GvYiEQQFO/lTypYjtE3XxjtXGRIA+4
i1t20Lmy2Scl1WV+LMbhk0i+sEBOzuD5jctQWqX68KYYUqWbiqULsrdHtgodITDG
CrlVvwB9BACb2JVDjbUhY3VDl3yHopmGM0kMZGLUvRaJOnCh2Dc+B/cCt/iiO6UB
ABEBAAGJAjYEGAEKACAWIQR1FhpVFCa+EmlQPOaASMo35B6xbQUCYsK5ZwIbDAAK
CRCASMo35B6xbRuWEACFH9cR36izop9hOu7oEnwLABGC6U4mipTvgKD4wu1SS0U4
NRPzkTm7FGxXy2QPbDOj1/G7dvHc7fGzQimofGRIW/4/GVWRQi2pQJwKTP1KlRid
e9oFG1+MTc6o1ZBkhz1GQbMGxeu9Na0c0DzXGMrsP6G75WSUEX+5srXuJtrxRrBy
E3BICzn5YyWTT2cLgN3AucalL04TIqGiocvi3X/n03CXe2M2mbJFo1y1bl6M/tmR
0fktpVlCDRs6EphMXHsKSTtO6pKPN6M0Lg50vS512xNGyc/tL2aJw/snO0vMDHSU
JDebH4LjV1RmTRnUJxERDCfe8Jh1bsIa4u8DcVmoLyjTMXJrPNUycYIkyfp9mwAX
LjBY2mW6Qyfp3IVdQUS1N8okIk1TiHIZC7DXdXuH7WnfEDDTV9fY91jY835y8stj
lpX0UGjm+npt2Vyth9kIQtgUnnBl76PdjAmCHztEaL1SXiQh6h2DhdkfeVX4GUrF
BE02K7Qy6ERZo7274WLlJOiW5EyyOKVHYrSzRLajsh8xHus270FhfcKRQG+LO8f+
2ecvNRuNZcMiwZDUwSzUkabXk8C9F/3EOcUnHoznh/g//Z17/ZktIuJy4DLLkYIk
jtPL12rsffEIbEo7Ok/ntyVf5rgitiHu5xSTzjG6cj0olm46rsJ0h3hij58MOw==
=3bbH
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,28 +0,0 @@
//go:build !windows
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package docker
import (
"os"
"golang.org/x/sys/unix"
)
func isInteractive() bool {
_, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
return err == nil
}

View File

@ -1,36 +0,0 @@
//go:build windows
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package docker
import (
"os"
"golang.org/x/sys/windows"
)
func isInteractive() bool {
handle := windows.Handle(os.Stdout.Fd())
var mode uint32
if err := windows.GetConsoleMode(handle, &mode); err != nil {
return false
}
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
if err := windows.SetConsoleMode(handle, mode); err != nil {
return false
}
return true
}

View File

@ -15,27 +15,13 @@
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)
@ -49,122 +35,3 @@ 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, pull bool, tag, dockerfile, dir, platform string, buildArgs ...string) error {
if dockerfile == "" {
dockerfile = filepath.Join(dir, "Dockerfile")
}
args := []string{"image", "build", "-t", tag, "-f", dockerfile, "--platform", platform}
if pull {
args = append(args, "--pull")
}
for _, v := range buildArgs {
args = append(args, "--build-arg", v)
}
args = append(args, dir)
return Cmd(ctx, args...)
}
func Tag(ctx context.Context, img string, tags ...string) error {
if len(tags) == 0 {
return fmt.Errorf("no tags specified")
}
args := []string{"image", "tag"}
for _, tag := range tags {
if err := Cmd(ctx, append(args, img, tag)...); err != nil {
return err
}
}
return nil
}
func Remove(ctx context.Context, tag string) error {
return Cmd(ctx, "image", "rm", tag)
}
func ImageList(ctx context.Context, tag string) ([]string, error) {
o, _, err := CmdOut(ctx, "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", tag)
if err != nil {
return nil, err
}
s := bufio.NewScanner(strings.NewReader(o))
var imgs []string
for s.Scan() {
imgs = append(imgs, s.Text())
}
return imgs, s.Err()
}
func ImageSave(ctx context.Context, tag, file string) error {
return Cmd(ctx, "image", "save", "-o", file, tag)
}
func Pull(ctx context.Context, platform, tag string) error {
return Cmd(ctx, "image", "pull", "--platform", platform, tag)
}
func Push(ctx context.Context, tag string) error {
return Cmd(ctx, "image", "push", tag)
}
func RunInteractiveAndRemove(ctx context.Context, args ...string) error {
cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func RunAndRemove(ctx context.Context, args ...string) error {
logrus.Tracef("running 'docker run --rm %s'", strings.Join(args, " "))
return Cmd(ctx, append([]string{"run", "--rm"}, args...)...)
}
func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...string) error {
pwd, err := os.Getwd()
if err != nil {
return err
}
if in == "" {
in = pwd
}
if out == "" {
out = pwd
}
if image == "" {
image = "linkacloud/d2vm"
}
if version == "" {
version = "latest"
}
a := []string{"run", "--rm"}
interactive := isInteractive()
if interactive {
a = append(a, "-i", "-t")
}
a = append(a,
"--privileged",
"-e",
// yes... it is kind of a dirty hack
fmt.Sprintf("SUDO_UID=%d", os.Getuid()),
"-v",
fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()),
"-v",
fmt.Sprintf("%s:/in", in),
"-v",
fmt.Sprintf("%s:/out", out),
"-w",
"/d2vm",
fmt.Sprintf("%s:%s", image, version),
cmd,
)
c := exec.CommandContext(ctx, "docker", append(a, args...)...)
if interactive {
c.Stdin = os.Stdin
}
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}

View File

@ -18,35 +18,21 @@ import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/sirupsen/logrus"
)
var (
Run = RunNoOut
CommandContext = exec.CommandContext
)
func SetDebug(debug bool) {
if debug {
Run = RunDebug
logrus.SetLevel(logrus.DebugLevel)
} else {
Run = RunNoOut
}
}
func CommandContext(ctx context.Context, c string, args ...string) *exec.Cmd {
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
return exec.CommandContext(ctx, c, args...)
}
func RunDebug(ctx context.Context, c string, args ...string) error {
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
func RunStdout(ctx context.Context, c string, args ...string) error {
cmd := exec.CommandContext(ctx, c, args...)
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -1,148 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package qemu
import (
"io"
"github.com/google/uuid"
)
type Option func(c *config)
type Disk struct {
Path string
Size int
Format string
}
type PublishedPort struct {
Guest uint16
Host uint16
Protocol string
}
// config contains the config for Qemu
type config struct {
path string
uuid uuid.UUID
gui bool
disks []Disk
networking string
arch string
cpus uint
memory uint
bios string
accel string
detached bool
qemuBinPath string
qemuImgPath string
publishedPorts []PublishedPort
netdevConfig string
stdin io.Reader
stdout io.Writer
stderr io.Writer
}
func WithGUI() Option {
return func(c *config) {
c.gui = true
}
}
func WithDisks(disks ...Disk) Option {
return func(c *config) {
c.disks = disks
}
}
func WithNetworking(networking string) Option {
return func(c *config) {
c.networking = networking
}
}
func WithArch(arch string) Option {
return func(c *config) {
c.arch = arch
}
}
func WithCPUs(cpus uint) Option {
return func(c *config) {
c.cpus = cpus
}
}
func WithMemory(memory uint) Option {
return func(c *config) {
c.memory = memory
}
}
func WithBios(bios string) Option {
return func(c *config) {
c.bios = bios
}
}
func WithAccel(accel string) Option {
return func(c *config) {
c.accel = accel
}
}
func WithDetached() Option {
return func(c *config) {
c.detached = true
}
}
func WithQemuBinPath(path string) Option {
return func(c *config) {
c.qemuBinPath = path
}
}
func WithQemuImgPath(path string) Option {
return func(c *config) {
c.qemuImgPath = path
}
}
func WithPublishedPorts(ports ...PublishedPort) Option {
return func(c *config) {
c.publishedPorts = ports
}
}
func WithStdin(r io.Reader) Option {
return func(c *config) {
c.stdin = r
}
}
func WithStdout(w io.Writer) Option {
return func(c *config) {
c.stdout = w
}
}
func WithStderr(w io.Writer) Option {
return func(c *config) {
c.stderr = w
}
}

View File

@ -1,365 +0,0 @@
package qemu
import (
"context"
"crypto/rand"
"fmt"
"net"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
const (
NetworkingNone = "none"
NetworkingUser = "user"
NetworkingTap = "tap"
NetworkingBridge = "bridge"
NetworkingDefault = NetworkingUser
)
var (
defaultArch string
defaultAccel string
)
func init() {
switch runtime.GOARCH {
case "arm64":
defaultArch = "aarch64"
case "amd64":
defaultArch = "x86_64"
case "s390x":
defaultArch = "s390x"
}
switch {
case runtime.GOARCH == "s390x":
defaultAccel = "kvm"
case haveKVM():
defaultAccel = "kvm:tcg"
case runtime.GOOS == "darwin":
defaultAccel = "hvf:tcg"
}
}
func Run(ctx context.Context, path string, opts ...Option) error {
config := &config{}
for _, o := range opts {
o(config)
}
config.path = path
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
config.uuid = uuid.New()
// These envvars override the corresponding command line
// options. So this must remain after the `flags.Parse` above.
// accel = GetStringValue("LINUXKIT_QEMU_ACCEL", accel, "")
if config.arch == "" {
config.arch = defaultArch
}
if config.accel == "" {
config.accel = defaultAccel
}
if _, err := os.Stat(config.path); err != nil {
return err
}
if config.cpus == 0 {
config.cpus = 1
}
if config.memory == 0 {
config.memory = 1024
}
for i, d := range config.disks {
id := ""
if i != 0 {
id = strconv.Itoa(i)
}
if d.Size != 0 && d.Format == "" {
d.Format = "qcow2"
}
if d.Size != 0 && d.Path == "" {
d.Path = "disk" + id + ".img"
}
if d.Path == "" {
return fmt.Errorf("disk specified with no size or name")
}
config.disks[i] = d
}
config.disks = append([]Disk{{Path: config.path}}, config.disks...)
if config.networking == "" || config.networking == "default" {
dflt := NetworkingDefault
config.networking = dflt
}
netMode := strings.SplitN(config.networking, ",", 2)
switch netMode[0] {
case NetworkingUser:
config.netdevConfig = "user,id=t0"
case NetworkingTap:
if len(netMode) != 2 {
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingTap)
}
if len(config.publishedPorts) != 0 {
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
}
config.netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
case NetworkingBridge:
if len(netMode) != 2 {
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingBridge)
}
if len(config.publishedPorts) != 0 {
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
}
config.netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
case NetworkingNone:
if len(config.publishedPorts) != 0 {
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
}
config.netdevConfig = ""
default:
return fmt.Errorf("Invalid networking mode: %s", netMode[0])
}
if err := config.discoverBinaries(); err != nil {
log.Fatal(err)
}
return config.runQemuLocal(ctx)
}
func (c *config) runQemuLocal(ctx context.Context) (err error) {
var args []string
args, err = c.buildQemuCmdline()
if err != nil {
return err
}
for _, d := range c.disks {
// If disk doesn't exist then create one
if _, err := os.Stat(d.Path); err != nil {
if os.IsNotExist(err) {
log.Debugf("Creating new qemu disk [%s] format %s", d.Path, d.Format)
qemuImgCmd := exec.Command(c.qemuImgPath, "create", "-f", d.Format, d.Path, fmt.Sprintf("%dM", d.Size))
log.Debugf("%v", qemuImgCmd.Args)
if err := qemuImgCmd.Run(); err != nil {
return fmt.Errorf("Error creating disk [%s] format %s: %s", d.Path, d.Format, err.Error())
}
} else {
return err
}
} else {
log.Infof("Using existing disk [%s] format %s", d.Path, d.Format)
}
}
// Detached mode is only supported in a container.
if c.detached == true {
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
}
qemuCmd := exec.CommandContext(ctx, c.qemuBinPath, args...)
// If verbosity is enabled print out the full path/arguments
log.Debugf("%v", qemuCmd.Args)
// If we're not using a separate window then link the execution to stdin/out
if c.gui == true {
qemuCmd.Stdin = nil
qemuCmd.Stdout = nil
qemuCmd.Stderr = nil
} else {
qemuCmd.Stdin = c.stdin
qemuCmd.Stdout = c.stdout
qemuCmd.Stderr = c.stderr
}
return qemuCmd.Run()
}
func (c *config) buildQemuCmdline() ([]string, error) {
// Iterate through the flags and build arguments
var qemuArgs []string
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", c.cpus))
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", c.memory))
qemuArgs = append(qemuArgs, "-uuid", c.uuid.String())
if c.bios != "" {
qemuArgs = append(qemuArgs, "-bios", c.bios)
}
// Need to specify the vcpu type when running qemu on arm64 platform, for security reason,
// the vcpu should be "host" instead of other names such as "cortex-a53"...
if c.arch == "aarch64" && runtime.GOARCH != "arm64" {
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
} else {
qemuArgs = append(qemuArgs, "-cpu", "host")
}
// goArch is the GOARCH equivalent of config.Arch
var goArch string
switch c.arch {
case "s390x":
goArch = "s390x"
case "aarch64":
goArch = "arm64"
case "x86_64":
goArch = "amd64"
default:
return nil, fmt.Errorf("%s is an unsupported architecture.", c.arch)
}
if goArch != runtime.GOARCH {
log.Infof("Disable acceleration as %s != %s", c.arch, runtime.GOARCH)
c.accel = ""
}
if c.accel != "" {
switch c.arch {
case "s390x":
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", c.accel))
case "aarch64":
gic := ""
// VCPU supports less PA bits (36) than requested by the memory map (40)
highmem := "highmem=off,"
if runtime.GOOS == "linux" {
// gic-version=host requires KVM, which implies Linux
gic = "gic_version=host,"
highmem = ""
}
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("virt,%s%saccel=%s", gic, highmem, c.accel))
default:
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", c.accel))
}
} else {
switch c.arch {
case "s390x":
qemuArgs = append(qemuArgs, "-machine", "s390-ccw-virtio")
case "aarch64":
qemuArgs = append(qemuArgs, "-machine", "virt")
default:
qemuArgs = append(qemuArgs, "-machine", "q35")
}
}
// rng-random does not work on macOS
// Temporarily disable it until fixed upstream.
if runtime.GOOS != "darwin" {
rng := "rng-random,id=rng0"
if runtime.GOOS == "linux" {
rng = rng + ",filename=/dev/urandom"
}
if c.arch == "s390x" {
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-ccw,rng=rng0")
} else {
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-pci,rng=rng0")
}
}
var lastDisk int
for i, d := range c.disks {
index := i
if d.Format != "" {
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",format="+d.Format+",index="+strconv.Itoa(index)+",media=disk")
} else {
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",index="+strconv.Itoa(index)+",media=disk")
}
lastDisk = index
}
// Ensure CDROMs start from at least hdc
if lastDisk < 2 {
lastDisk = 2
}
if c.netdevConfig != "" {
mac := generateMAC()
if c.arch == "s390x" {
qemuArgs = append(qemuArgs, "-device", "virtio-net-ccw,netdev=t0,mac="+mac.String())
} else {
qemuArgs = append(qemuArgs, "-device", "virtio-net-pci,netdev=t0,mac="+mac.String())
}
forwardings, err := buildQemuForwardings(c.publishedPorts)
if err != nil {
log.Error(err)
}
qemuArgs = append(qemuArgs, "-netdev", c.netdevConfig+forwardings)
} else {
qemuArgs = append(qemuArgs, "-net", "none")
}
if c.gui != true {
qemuArgs = append(qemuArgs, "-nographic")
}
return qemuArgs, nil
}
func (c *config) discoverBinaries() error {
if c.qemuImgPath != "" {
return nil
}
qemuBinPath := "qemu-system-" + c.arch
qemuImgPath := "qemu-img"
var err error
c.qemuBinPath, err = exec.LookPath(qemuBinPath)
if err != nil {
return fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
}
c.qemuImgPath, err = exec.LookPath(qemuImgPath)
if err != nil {
return fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
}
return nil
}
func buildQemuForwardings(publishedPorts []PublishedPort) (string, error) {
if len(publishedPorts) == 0 {
return "", nil
}
var forwardings string
for _, p := range publishedPorts {
hostPort := p.Host
guestPort := p.Guest
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
}
return forwardings, nil
}
func haveKVM() bool {
_, err := os.Stat("/dev/kvm")
return !os.IsNotExist(err)
}
func generateMAC() net.HardwareAddr {
mac := make([]byte, 6)
n, err := rand.Read(mac)
if err != nil {
log.WithError(err).Fatal("failed to generate random mac address")
}
if n != 6 {
log.WithError(err).Fatalf("generated %d bytes for random mac address", n)
}
mac[0] &^= 0x01 // Clear multicast bit
mac[0] |= 0x2 // Set locally administered bit
return mac
}

View File

@ -1,114 +0,0 @@
// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package qemu_img
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"go.linka.cloud/d2vm/pkg/docker"
exec2 "go.linka.cloud/d2vm/pkg/exec"
)
var (
DockerImageName string
DockerImageVersion string
)
type ImgInfo struct {
VirtualSize int `json:"virtual-size"`
Filename string `json:"filename"`
Format string `json:"format"`
ActualSize int `json:"actual-size"`
DirtyFlag bool `json:"dirty-flag"`
}
func Info(ctx context.Context, in string) (*ImgInfo, error) {
var (
o []byte
err error
)
if path, _ := exec.LookPath("qemu-img"); path == "" {
inAbs, err := filepath.Abs(in)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path for %q: %v", path, err)
}
inMount := filepath.Dir(inAbs)
in := filepath.Join("/in", filepath.Base(inAbs))
o, err = exec2.CommandContext(
ctx,
"docker",
"run",
"--rm",
"-v",
inMount+":/in",
"--entrypoint",
"qemu-img",
fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion),
"info",
in,
"--output",
"json",
).CombinedOutput()
} else {
o, err = exec2.CommandContext(ctx, "qemu-img", "info", in, "--output", "json").CombinedOutput()
}
if err != nil {
return nil, fmt.Errorf("%v: %s", err, string(o))
}
var i ImgInfo
if err := json.Unmarshal(o, &i); err != nil {
return nil, err
}
return &i, nil
}
func Convert(ctx context.Context, format, in, out string) error {
if path, _ := exec.LookPath("qemu-img"); path != "" {
return exec2.Run(ctx, "qemu-img", "convert", "-O", format, in, out)
}
inAbs, err := filepath.Abs(in)
if err != nil {
return fmt.Errorf("failed to get absolute path for %q: %v", in, err)
}
inMount := filepath.Dir(inAbs)
in = filepath.Join("/in", filepath.Base(inAbs))
outAbs, err := filepath.Abs(out)
if err != nil {
return fmt.Errorf("failed to get absolute path for %q: %v", out, err)
}
outMount := filepath.Dir(outAbs)
out = filepath.Join("/out", filepath.Base(outAbs))
return docker.RunAndRemove(
ctx,
"-v",
fmt.Sprintf("%s:/in", inMount),
"-v",
fmt.Sprintf("%s:/out", outMount),
"--entrypoint",
"qemu-img",
fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion),
"convert",
"-O",
format,
in,
out,
)
}

View File

@ -1,75 +0,0 @@
#!/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

View File

@ -1,220 +0,0 @@
#!/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

View File

@ -1,51 +0,0 @@
#!/usr/bin/env bash
. $HOME/demo-magic
TYPE_SPEED=20
EXEC_WAIT=1
DEMO_PROMPT="${PURPLE}➜ ${PURPLE}\W "
defer_kill_htop() {
sleep 8
pkill htop
}
resize
print_prompt
sleep 2
pei "# Nice auto login ;)"
PROMPT_TIMEOUT=1
wait
if ! $(ps aux|grep -e "sshd: adphi" | grep -v grep &> /dev/null); then
pei "# Is the network configured ?"
pei "ip a"
pei "# But is it trully working ?"
pei "ping -c 5 linka.cloud"
fi
pei "# Now let's take a look at CPU and Memory usage..."
wait
defer_kill_htop &
pei "htop"
pei "# Let's see disk usage..."
PROMPT_TIMEOUT=3
pei "df -hT"
wait
pei "# Pretty small right ? ;)"
PROMPT_TIMEOUT=1
wait
pei "sudo poweroff"

View File

@ -1,100 +0,0 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/exec"
)
const syslinuxCfg = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL %s
APPEND %s
`
var mbrPaths = []string{
// debian path
"/usr/lib/syslinux/mbr/mbr.bin",
// ubuntu path
"/usr/lib/EXTLINUX/mbr.bin",
// alpine path
"/usr/share/syslinux/mbr.bin",
// centos path
"/usr/share/syslinux/mbr.bin",
// archlinux path
"/usr/lib/syslinux/bios/mbr.bin",
}
type syslinux struct {
c Config
mbrBin string
}
func (s syslinux) Validate(_ BootFS) error {
return nil
}
func (s syslinux) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up syslinux bootloader")
if err := exec.Run(ctx, "extlinux", "--install", filepath.Join(root, "boot")); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(root, "boot", "syslinux.cfg"), []byte(fmt.Sprintf(syslinuxCfg, s.c.Kernel, cmdline)), perm); err != nil {
return err
}
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", s.mbrBin), fmt.Sprintf("of=%s", dev), "bs=440", "count=1", "conv=notrunc"); err != nil {
return err
}
return nil
}
type syslinuxProvider struct{}
func (s syslinuxProvider) New(c Config, _ OSRelease, arch string) (Bootloader, error) {
if arch != "x86_64" {
return nil, fmt.Errorf("syslinux is only supported for amd64")
}
mbrBin := ""
for _, v := range mbrPaths {
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
}
}
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
}
return &syslinux{
c: c,
mbrBin: mbrBin,
}, nil
}
func (s syslinuxProvider) Name() string {
return "syslinux"
}
func init() {
RegisterBootloaderProvider(syslinuxProvider{})
}

View File

@ -2,49 +2,22 @@ FROM {{ .Image }}
USER root
RUN apk add --no-cache \
RUN apk update --no-cache && \
apk add \
util-linux \
linux-virt \
{{- if ge .Release.VersionID "3.17" }}
busybox-openrc \
busybox-mdev-openrc \
busybox-extras-openrc \
busybox-mdev-openrc \
{{- else }}
busybox-initscripts \
{{- end }}
openrc && \
find /boot -type l -exec rm {} \;
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
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{ 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 }}
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd
{{ if .Luks }}
RUN apk add --no-cache cryptsetup && \
source /etc/mkinitfs/mkinitfs.conf && \
echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \
mkinitfs $(ls /lib/modules)
{{- end }}
# we need to keep that at the end, because after it, we can't install packages without error anymore due to grub hooks
{{- if .Grub }}
RUN apk add --no-cache \
{{- if .GrubBIOS }}
grub-bios \
{{- end }}
{{- if .GrubEFI }}
grub-efi \
{{- end }}
grub
{{- end }}

View File

@ -2,47 +2,16 @@ FROM {{ .Image }}
USER root
{{ $version := atoi .Release.Version }}
{{ if le $version 8 }}
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
{{ end }}
RUN yum update -y
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
RUN yum install -y \
kernel \
systemd \
NetworkManager \
{{- if .GrubBIOS }}
grub2 \
{{- end }}
{{- if .GrubEFI }}
grub2 grub2-efi-x64 grub2-efi-x64-modules \
{{- end }}
e2fsprogs \
sudo && \
systemctl enable NetworkManager && \
systemctl unmask systemd-remount-fs.service && \
systemctl unmask getty.target && \
find /boot -type l -exec rm {} \;
RUN yum install -y kernel systemd sudo
{{ if .Luks }}
RUN yum install -y cryptsetup && \
dracut --no-hostonly --regenerate-all --force --install="/usr/sbin/cryptsetup"
{{ else }}
RUN dracut --no-hostonly --regenerate-all --force
{{ end }}
RUN dracut --no-hostonly --regenerate-all --force && \
cd /boot && \
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
ln -s $(find . -name 'initramfs-*.img') initrd.img
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{- if not .Grub }}
RUN cd /boot && \
mv $(find {{ if le $version 8 }}.{{ else }}/{{ end }} -name 'vmlinuz*') /boot/vmlinuz && \
mv $(find . -name 'initramfs-*.img') /boot/initrd.img
{{- end }}
RUN yum clean all && \
rm -rf /var/cache/yum
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd

View File

@ -2,78 +2,18 @@ FROM {{ .Image }}
USER root
{{- if eq .Release.VersionID "9" }}
RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list && \
echo "deb-src http://archive.debian.org/debian stretch main" >> /etc/apt/sources.list && \
echo "deb http://archive.debian.org/debian stretch-backports main" >> /etc/apt/sources.list && \
echo "deb http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list && \
echo "deb-src http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list
{{- end }}
RUN apt-get update && \
RUN apt-get -y update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-amd64 && \
find /boot -type l -exec rm {} \;
linux-image-amd64
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
systemd-sysv \
systemd \
{{- if .Grub }}
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-${ARCH}-bin \
{{- end }}
dbus \
iproute2 \
isc-dhcp-client \
udhcpc \
iputils-ping
RUN systemctl preset-all
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{ if eq .NetworkManager "netplan" }}
RUN apt install -y netplan.io
RUN mkdir -p /etc/netplan && printf '\
network:\n\
version: 2\n\
renderer: networkd\n\
ethernets:\n\
eth0:\n\
dhcp4: true\n\
dhcp-identifier: mac\n\
nameservers:\n\
addresses:\n\
- 8.8.8.8\n\
- 8.8.4.4\n\
' > /etc/netplan/00-netcfg.yaml
{{ else if eq .NetworkManager "ifupdown"}}
RUN if [ -z "$(apt-cache madison ifupdown2 2> /dev/nul)" ]; then apt install -y ifupdown; else apt install -y ifupdown2; fi
RUN mkdir -p /etc/network && printf '\
auto eth0\n\
allow-hotplug eth0\n\
iface eth0 inet dhcp\n\
' > /etc/network/interfaces
{{ end }}
{{- if .Luks }}
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook && \
update-initramfs -u -v
{{- end }}
# needs to be after update-initramfs
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd

View File

@ -2,69 +2,19 @@ FROM {{ .Image }}
USER root
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-virtual \
initramfs-tools \
systemd-sysv \
systemd \
{{- if .Grub }}
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-${ARCH}-bin \
{{- end }}
dbus \
isc-dhcp-client \
iproute2 \
iputils-ping && \
find /boot -type l -exec rm {} \;
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
{{ if gt .Release.VersionID "16.04" }}
RUN systemctl preset-all
{{ end }}
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{ if eq .NetworkManager "netplan" }}
RUN apt install -y netplan.io
RUN mkdir -p /etc/netplan && printf '\
network:\n\
version: 2\n\
renderer: networkd\n\
ethernets:\n\
eth0:\n\
dhcp4: true\n\
dhcp-identifier: mac\n\
nameservers:\n\
addresses:\n\
- 8.8.8.8\n\
- 8.8.4.4\n\
' > /etc/netplan/00-netcfg.yaml
{{ else if eq .NetworkManager "ifupdown"}}
RUN if [ -z "$(apt-cache madison ifupdown-ng 2> /dev/nul)" ]; then apt install -y ifupdown; else apt install -y ifupdown-ng; fi
RUN mkdir -p /etc/network && printf '\
auto eth0\n\
allow-hotplug eth0\n\
iface eth0 inet dhcp\n\
' > /etc/network/interfaces
{{ end }}
{{- if .Luks }}
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
update-initramfs -u -v
{{- end }}
# needs to be after update-initramfs
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN echo "root:{{- if .Password}}{{ .Password}}{{- else}}root{{- end}}" | chpasswd

View File

@ -1,34 +1,6 @@
// 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 (
"fmt"
"runtime"
"go.linka.cloud/d2vm/pkg/qemu_img"
)
var (
Arch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
Version = ""
BuildDate = ""
Image = "linkacloud/d2vm"
)
func init() {
qemu_img.DockerImageName = Image
qemu_img.DockerImageVersion = Version
}

View File

@ -3,4 +3,4 @@
IMG=${1:-disk0.qcow2}
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0' --transient
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'