mirror of
https://github.com/linka-cloud/d2vm.git
synced 2024-09-27 23:04:19 +00:00
Compare commits
No commits in common. "main" and "v0.1.0-rc3" have entirely different histories.
main
...
v0.1.0-rc3
@ -7,5 +7,3 @@ bin
|
|||||||
dist
|
dist
|
||||||
images
|
images
|
||||||
examples/build
|
examples/build
|
||||||
e2e
|
|
||||||
**/*_test.go
|
|
||||||
|
120
.github/workflows/ci.yaml
vendored
120
.github/workflows/ci.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Set up QEMU dependency
|
- name: Set up QEMU dependency
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@ -30,7 +30,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
|
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils
|
||||||
|
|
||||||
- name: Share cache with other actions
|
- name: Share cache with other actions
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
@ -45,101 +45,6 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || make 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:
|
docs-up-to-date:
|
||||||
name: Docs up to date
|
name: Docs up to date
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -154,7 +59,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Share cache with other actions
|
- name: Share cache with other actions
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
@ -183,7 +88,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Set up QEMU dependency
|
- name: Set up QEMU dependency
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@ -214,7 +119,6 @@ jobs:
|
|||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v4
|
uses: crazy-max/ghaction-import-gpg@v4
|
||||||
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_KEY }}
|
gpg_private_key: ${{ secrets.GPG_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSWORD }}
|
passphrase: ${{ secrets.GPG_PASSWORD }}
|
||||||
@ -223,7 +127,6 @@ jobs:
|
|||||||
run: make build-snapshot
|
run: make build-snapshot
|
||||||
|
|
||||||
- name: Release Snapshot
|
- name: Release Snapshot
|
||||||
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }}
|
||||||
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
|
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
|
||||||
@ -232,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
name: Build Docker Image
|
name: Build Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -243,7 +146,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Set up QEMU dependency
|
- name: Set up QEMU dependency
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@ -277,14 +180,12 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release Binaries
|
name: Release Binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-18.04
|
||||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
needs:
|
needs:
|
||||||
- tests
|
- tests
|
||||||
- templates-tests
|
|
||||||
- docs-up-to-date
|
- docs-up-to-date
|
||||||
- build
|
- build
|
||||||
- e2e-tests
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -296,7 +197,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Share cache with other actions
|
- name: Share cache with other actions
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
@ -327,13 +228,12 @@ jobs:
|
|||||||
|
|
||||||
release-image:
|
release-image:
|
||||||
name: Release Docker Image
|
name: Release Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-18.04
|
||||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
needs:
|
needs:
|
||||||
- tests
|
- tests
|
||||||
- docs-up-to-date
|
- docs-up-to-date
|
||||||
- build-image
|
- build-image
|
||||||
- e2e-tests
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -345,7 +245,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Set up QEMU dependency
|
- name: Set up QEMU dependency
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,17 +4,14 @@ scratch
|
|||||||
*.qcow2
|
*.qcow2
|
||||||
*.vmdk
|
*.vmdk
|
||||||
*.vdi
|
*.vdi
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
images
|
images
|
||||||
/d2vm
|
/d2vm
|
||||||
/examples/build
|
/examples/build
|
||||||
/examples/full/demo-magic
|
|
||||||
/examples/full/inside
|
|
||||||
.goreleaser.yaml
|
.goreleaser.yaml
|
||||||
docs/build
|
docs/build
|
||||||
docs-src
|
docs-src
|
||||||
/completions
|
/completions
|
||||||
/cmd/d2vm/run/sparsecat-linux-*
|
/cmd/d2vm/run/sparsecat-linux-amd64
|
||||||
|
11
Dockerfile
11
Dockerfile
@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
FROM golang:1.20 as builder
|
FROM golang as builder
|
||||||
|
|
||||||
WORKDIR /d2vm
|
WORKDIR /d2vm
|
||||||
|
|
||||||
@ -33,16 +33,11 @@ RUN apt-get update && \
|
|||||||
util-linux \
|
util-linux \
|
||||||
udev \
|
udev \
|
||||||
parted \
|
parted \
|
||||||
kpartx \
|
|
||||||
e2fsprogs \
|
e2fsprogs \
|
||||||
dosfstools \
|
|
||||||
mount \
|
mount \
|
||||||
tar \
|
tar \
|
||||||
"$([ "$(uname -m)" = "x86_64" ] && echo extlinux)" \
|
extlinux \
|
||||||
cryptsetup-bin \
|
qemu-utils
|
||||||
qemu-utils && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||||
|
|
||||||
|
17
Makefile
17
Makefile
@ -64,19 +64,11 @@ docker-run:
|
|||||||
.PHONY: tests
|
.PHONY: tests
|
||||||
tests:
|
tests:
|
||||||
@go generate ./...
|
@go generate ./...
|
||||||
@go list .| xargs go test -exec sudo -count=1 -timeout 60m -v -skip TestConfig
|
@go list ./...| xargs go test -exec sudo -count=1 -timeout 20m -v
|
||||||
|
|
||||||
.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:
|
docs-up-to-date:
|
||||||
@$(MAKE) cli-docs
|
@$(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)
|
@git diff --quiet -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md' || (git --no-pager diff -- docs ':(exclude)docs/content/reference/d2vm_run_qemu.md'; echo "Please regenerate the documentation with 'make docs'"; exit 1)
|
||||||
|
|
||||||
check-fmt:
|
check-fmt:
|
||||||
@[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
@[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
||||||
@ -87,12 +79,11 @@ vet:
|
|||||||
build-dev: docker-build .build
|
build-dev: docker-build .build
|
||||||
|
|
||||||
install: docker-build
|
install: docker-build
|
||||||
@go generate ./...
|
|
||||||
@go install -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
@go install -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
||||||
|
|
||||||
.build:
|
.build:
|
||||||
@go generate ./...
|
@go generate ./...
|
||||||
@CGO_ENABLED=0 go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
@go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
|
||||||
|
|
||||||
.PHONY: build-snapshot
|
.PHONY: build-snapshot
|
||||||
build-snapshot: bin
|
build-snapshot: bin
|
||||||
@ -121,7 +112,7 @@ completions: .build
|
|||||||
.PHONY: examples
|
.PHONY: examples
|
||||||
examples: build-dev
|
examples: build-dev
|
||||||
@mkdir -p examples/build
|
@mkdir -p examples/build
|
||||||
@for f in $$(find examples -maxdepth 1 -type f -name '*Dockerfile'); do \
|
@for f in $$(find examples -type f -name '*Dockerfile' -maxdepth 1); do \
|
||||||
echo "Building $$f"; \
|
echo "Building $$f"; \
|
||||||
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
|
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
|
||||||
done
|
done
|
||||||
|
108
README.md
108
README.md
@ -19,19 +19,14 @@ Many thanks to him.
|
|||||||
|
|
||||||
**Only building Linux Virtual Machine images is supported.**
|
**Only building Linux Virtual Machine images is supported.**
|
||||||
|
|
||||||
Starting from v0.1.0, **d2vm** automatically run build and convert commands inside Docker when not running on linux
|
**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.
|
|
||||||
|
|
||||||
*Note: windows should be working, but is totally untested.*
|
|
||||||
|
|
||||||
## Supported VM Linux distributions:
|
## Supported VM Linux distributions:
|
||||||
|
|
||||||
Working and tested:
|
Working and tested:
|
||||||
|
|
||||||
- [x] Ubuntu (18.04+)
|
- [x] Ubuntu (18.04+)
|
||||||
Luks support is available only on Ubuntu 20.04+
|
|
||||||
- [x] Debian (stretch+)
|
- [x] Debian (stretch+)
|
||||||
Luks support is available only on Debian buster+
|
|
||||||
- [x] Alpine
|
- [x] Alpine
|
||||||
- [x] CentOS (8+)
|
- [x] CentOS (8+)
|
||||||
|
|
||||||
@ -42,71 +37,23 @@ Unsupported:
|
|||||||
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
||||||
if the file is missing, the build cannot succeed.
|
if the file is missing, the build cannot succeed.
|
||||||
|
|
||||||
Obviously, **Distroless** images are not supported.
|
Obviously, **Distroless** images are not supported.
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### osx
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/)
|
|
||||||
- [QEMU](https://www.qemu.org/download/#macos) (optional)
|
|
||||||
- [VirtualBox](https://www.virtualbox.org/wiki/Downloads) (optional)
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/)
|
|
||||||
- util-linux
|
|
||||||
- udev
|
|
||||||
- parted
|
|
||||||
- e2fsprogs
|
|
||||||
- dosfstools (when using fat32)
|
|
||||||
- mount
|
|
||||||
- tar
|
|
||||||
- extlinux (when using syslinux)
|
|
||||||
- qemu-utils
|
|
||||||
- cryptsetup (when using LUKS)
|
|
||||||
- [QEMU](https://www.qemu.org/download/#linux) (optional)
|
|
||||||
- [VirtualBox](https://www.virtualbox.org/wiki/Linux_Downloads) (optional)
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Install
|
### Install from release
|
||||||
|
|
||||||
#### 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).
|
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).
|
Extract the tarball:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VERSION=$(git ls-remote --tags https://github.com/linka-cloud/d2vm |cut -d'/' -f 3|tail -n 1)
|
tar -xvzf <RELEASE-TARBALL-NAME>.tar.gz
|
||||||
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
|
Move the extracted *d2vm* binary to somewhere in your `$PATH` (`/usr/local/bin` for most users).
|
||||||
|
|
||||||
|
### Install from source
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
@ -158,28 +105,20 @@ Usage:
|
|||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
--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
|
-f, --force Override output qcow2 image
|
||||||
--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
|
-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
|
--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")
|
-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
|
-p, --password string The Root user password (default "root")
|
||||||
--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
|
--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
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
-s, --size string The output image size (default "10G")
|
-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:
|
Global Flags:
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an image based on the **ubuntu** official image:
|
Create an image based on the **ubuntu** official image:
|
||||||
@ -316,28 +255,18 @@ Usage:
|
|||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
--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
|
--build-arg stringArray Set build-time variables
|
||||||
-f, --file string Name of the Dockerfile
|
-f, --file string Name of the Dockerfile
|
||||||
--force Override output qcow2 image
|
--force Override output image
|
||||||
-h, --help help for build
|
-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
|
--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")
|
-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
|
-p, --password string Root user password (default "root")
|
||||||
--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
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
-s, --size string The output image size (default "10G")
|
-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:
|
Global Flags:
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -352,13 +281,6 @@ Or if you want to create a VirtualBox image:
|
|||||||
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.vdi .
|
sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.vdi .
|
||||||
```
|
```
|
||||||
|
|
||||||
### KubeVirt Container Disk Images
|
|
||||||
|
|
||||||
Using the `--tag` flag with the `build` and `convert` commands, you can create a
|
|
||||||
[Container Disk Image](https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk) for [KubeVirt](https://kubevirt.io/).
|
|
||||||
|
|
||||||
The `--push` flag will push the image to the registry.
|
|
||||||
|
|
||||||
### Complete example
|
### Complete example
|
||||||
|
|
||||||
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
|
A complete example setting up a ZSH workstation is available in the [examples/full](examples/full/README.md) directory.
|
||||||
|
@ -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
|
|
||||||
}
|
|
384
builder.go
384
builder.go
@ -23,7 +23,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
@ -41,10 +40,70 @@ ff02::1 ip6-allnodes
|
|||||||
ff02::2 ip6-allrouters
|
ff02::2 ip6-allrouters
|
||||||
ff02::3 ip6-allhosts
|
ff02::3 ip6-allhosts
|
||||||
`
|
`
|
||||||
|
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 %s
|
||||||
|
`
|
||||||
|
syslinuxCfgDebian = `DEFAULT linux
|
||||||
|
SAY Now booting the kernel from SYSLINUX...
|
||||||
|
LABEL linux
|
||||||
|
KERNEL /vmlinuz
|
||||||
|
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
|
||||||
|
`
|
||||||
|
syslinuxCfgAlpine = `DEFAULT linux
|
||||||
|
SAY Now booting the kernel from SYSLINUX...
|
||||||
|
LABEL linux
|
||||||
|
KERNEL /boot/vmlinuz-virt
|
||||||
|
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200 %s
|
||||||
|
`
|
||||||
|
syslinuxCfgCentOS = `DEFAULT linux
|
||||||
|
SAY Now booting the kernel from SYSLINUX...
|
||||||
|
LABEL linux
|
||||||
|
KERNEL /boot/vmlinuz
|
||||||
|
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
|
||||||
|
|
||||||
|
mbrPaths = []string{
|
||||||
|
// debian path
|
||||||
|
"/usr/lib/syslinux/mbr/mbr.bin",
|
||||||
|
// ubuntu path
|
||||||
|
"/usr/lib/EXTLINUX/mbr.bin",
|
||||||
|
// alpine path
|
||||||
|
"/usr/share/syslinux/mbr.bin",
|
||||||
|
// centos path
|
||||||
|
"/usr/share/syslinux/mbr.bin",
|
||||||
|
// archlinux path
|
||||||
|
"/usr/lib/syslinux/bios/mbr.bin",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
perm os.FileMode = 0644
|
perm os.FileMode = 0644
|
||||||
)
|
)
|
||||||
|
|
||||||
var formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vhd", "vhdx", "vmdk"}
|
func sysconfig(osRelease OSRelease) (string, error) {
|
||||||
|
switch osRelease.ID {
|
||||||
|
case ReleaseUbuntu:
|
||||||
|
if osRelease.VersionID < "20.04" {
|
||||||
|
return syslinuxCfgDebian, nil
|
||||||
|
}
|
||||||
|
return syslinuxCfgUbuntu, nil
|
||||||
|
case ReleaseDebian:
|
||||||
|
return syslinuxCfgDebian, nil
|
||||||
|
case ReleaseAlpine:
|
||||||
|
return syslinuxCfgAlpine, nil
|
||||||
|
case ReleaseCentOS:
|
||||||
|
return syslinuxCfgCentOS, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("%s: distribution not supported", osRelease.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Builder interface {
|
type Builder interface {
|
||||||
Build(ctx context.Context) (err error)
|
Build(ctx context.Context) (err error)
|
||||||
@ -52,9 +111,7 @@ type Builder interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
osRelease OSRelease
|
osRelease OSRelease
|
||||||
config Config
|
|
||||||
bootloader Bootloader
|
|
||||||
|
|
||||||
src string
|
src string
|
||||||
img *image
|
img *image
|
||||||
@ -62,46 +119,20 @@ type builder struct {
|
|||||||
diskOut string
|
diskOut string
|
||||||
format string
|
format string
|
||||||
|
|
||||||
size uint64
|
size int64
|
||||||
mntPoint string
|
mntPoint string
|
||||||
|
|
||||||
splitBoot bool
|
mbrPath string
|
||||||
bootSize uint64
|
|
||||||
bootFS BootFS
|
|
||||||
|
|
||||||
loDevice string
|
|
||||||
bootPart string
|
|
||||||
rootPart string
|
|
||||||
cryptPart string
|
|
||||||
cryptRoot string
|
|
||||||
mappedCryptRoot string
|
|
||||||
bootUUID string
|
|
||||||
rootUUID string
|
|
||||||
cryptUUID string
|
|
||||||
|
|
||||||
luksPassword string
|
|
||||||
|
|
||||||
|
loDevice string
|
||||||
|
loPart string
|
||||||
|
diskUUD string
|
||||||
cmdLineExtra string
|
cmdLineExtra string
|
||||||
arch 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) {
|
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, osRelease OSRelease, format string, cmdLineExtra string) (Builder, error) {
|
||||||
var arch string
|
if err := checkDependencies(); err != nil {
|
||||||
switch platform {
|
return nil, err
|
||||||
case "linux/amd64":
|
|
||||||
arch = "x86_64"
|
|
||||||
case "linux/arm64", "linux/aarch64":
|
|
||||||
arch = "arm64"
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
|
|
||||||
}
|
|
||||||
if luksPassword != "" {
|
|
||||||
if !splitBoot {
|
|
||||||
return nil, fmt.Errorf("luks encryption requires split boot")
|
|
||||||
}
|
|
||||||
if !osRelease.SupportsLUKS() {
|
|
||||||
return nil, fmt.Errorf("luks encryption not supported on %s %s", osRelease.ID, osRelease.VersionID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
f := strings.ToLower(format)
|
f := strings.ToLower(format)
|
||||||
valid := false
|
valid := false
|
||||||
@ -113,55 +144,19 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
|
|||||||
if !valid {
|
if !valid {
|
||||||
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
|
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
|
||||||
}
|
}
|
||||||
if f == "vhd" {
|
|
||||||
f = "vpc"
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitBoot && bootSize < 50 {
|
mbrBin := ""
|
||||||
return nil, fmt.Errorf("boot partition size must be at least 50MiB")
|
for _, v := range mbrPaths {
|
||||||
|
if _, err := os.Stat(v); err == nil {
|
||||||
|
mbrBin = v
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if mbrBin == "" {
|
||||||
if splitBoot && bootSize >= size {
|
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
|
||||||
return nil, fmt.Errorf("boot partition size must be less than the disk size")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bootLoader == "" {
|
|
||||||
bootLoader = "syslinux"
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := osRelease.Config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitBoot {
|
|
||||||
config.Kernel = strings.TrimPrefix(config.Kernel, "/boot")
|
|
||||||
config.Initrd = strings.TrimPrefix(config.Initrd, "/boot")
|
|
||||||
}
|
|
||||||
|
|
||||||
if bootFS == "" {
|
|
||||||
bootFS = BootFSExt4
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bootFS.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
blp, err := BootloaderByName(bootLoader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bl, err := blp.New(config, osRelease, arch)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bl.Validate(bootFS); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 10 * uint64(datasize.GB)
|
size = 10 * int64(datasize.GB)
|
||||||
}
|
}
|
||||||
if disk == "" {
|
if disk == "" {
|
||||||
disk = "disk0"
|
disk = "disk0"
|
||||||
@ -181,23 +176,14 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
|
|||||||
// }
|
// }
|
||||||
b := &builder{
|
b := &builder{
|
||||||
osRelease: osRelease,
|
osRelease: osRelease,
|
||||||
config: config,
|
|
||||||
bootloader: bl,
|
|
||||||
img: img,
|
img: img,
|
||||||
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
|
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
|
||||||
diskOut: filepath.Join(workdir, disk+"."+format),
|
diskOut: filepath.Join(workdir, disk+"."+format),
|
||||||
format: f,
|
format: f,
|
||||||
size: size,
|
size: size,
|
||||||
|
mbrPath: mbrBin,
|
||||||
mntPoint: filepath.Join(workdir, "/mnt"),
|
mntPoint: filepath.Join(workdir, "/mnt"),
|
||||||
cmdLineExtra: cmdLineExtra,
|
cmdLineExtra: cmdLineExtra,
|
||||||
splitBoot: splitBoot,
|
|
||||||
bootSize: bootSize,
|
|
||||||
bootFS: bootFS,
|
|
||||||
luksPassword: luksPassword,
|
|
||||||
arch: arch,
|
|
||||||
}
|
|
||||||
if err := b.checkDependencies(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
|
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -233,12 +219,15 @@ func (b *builder) Build(ctx context.Context) (err error) {
|
|||||||
if err = b.setupRootFS(ctx); err != nil {
|
if err = b.setupRootFS(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.installBootloader(ctx); err != nil {
|
if err = b.installKernel(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = b.unmountImg(ctx); err != nil {
|
if err = b.unmountImg(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = b.setupMBR(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err = b.convert2Img(ctx); err != nil {
|
if err = b.convert2Img(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -261,21 +250,9 @@ func (b *builder) makeImg(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var args []string
|
if err := exec.Run(ctx, "parted", "-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"); err != nil {
|
||||||
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",
|
|
||||||
}
|
|
||||||
} 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,65 +263,15 @@ func (b *builder) mountImg(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.loDevice = strings.TrimSuffix(o, "\n")
|
b.loDevice = strings.TrimSuffix(o, "\n")
|
||||||
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
|
if err := exec.Run(ctx, "partprobe", b.loDevice); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.bootPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
|
b.loPart = fmt.Sprintf("%sp1", b.loDevice)
|
||||||
b.rootPart = ifElse(b.splitBoot, fmt.Sprintf("/dev/mapper/%sp2", filepath.Base(b.loDevice)), b.bootPart)
|
logrus.Infof("creating raw image file system")
|
||||||
if b.isLuksEnabled() {
|
if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil {
|
||||||
logrus.Infof("encrypting root partition")
|
|
||||||
f, err := os.CreateTemp("", "key")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
if _, err := f.WriteString(b.luksPassword); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// cryptsetup luksFormat --batch-mode --verify-passphrase --type luks2 $ROOT_DEVICE $KEY_FILE
|
|
||||||
if err := exec.Run(ctx, "cryptsetup", "luksFormat", "--batch-mode", "--type", "luks2", b.rootPart, f.Name()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.cryptRoot = fmt.Sprintf("d2vm-%s-root", uuid.New().String())
|
|
||||||
// cryptsetup open -d $KEY_FILE $ROOT_DEVICE $ROOT_LABEL
|
|
||||||
if err := exec.Run(ctx, "cryptsetup", "open", "--key-file", f.Name(), b.rootPart, b.cryptRoot); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.cryptPart = b.rootPart
|
|
||||||
b.rootPart = "/dev/mapper/root"
|
|
||||||
b.mappedCryptRoot = filepath.Join("/dev/mapper", b.cryptRoot)
|
|
||||||
logrus.Infof("creating raw image file system")
|
|
||||||
if err := exec.Run(ctx, "mkfs.ext4", b.mappedCryptRoot); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logrus.Infof("creating raw image file system")
|
|
||||||
if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !b.splitBoot {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if b.bootFS.IsFat() {
|
if err := exec.Run(ctx, "mount", b.loPart, b.mntPoint); err != nil {
|
||||||
err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart)
|
|
||||||
} else {
|
|
||||||
err = exec.Run(ctx, "mkfs.ext4", b.bootPart)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -353,18 +280,13 @@ func (b *builder) mountImg(ctx context.Context) error {
|
|||||||
func (b *builder) unmountImg(ctx context.Context) error {
|
func (b *builder) unmountImg(ctx context.Context) error {
|
||||||
logrus.Infof("unmounting raw image")
|
logrus.Infof("unmounting raw image")
|
||||||
var merr error
|
var merr error
|
||||||
if b.splitBoot {
|
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
|
||||||
merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")))
|
merr = multierr.Append(merr, err)
|
||||||
}
|
}
|
||||||
merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint))
|
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
|
||||||
if b.isLuksEnabled() {
|
merr = multierr.Append(merr, err)
|
||||||
merr = multierr.Append(merr, exec.Run(ctx, "cryptsetup", "close", b.mappedCryptRoot))
|
|
||||||
}
|
}
|
||||||
return multierr.Combine(
|
return merr
|
||||||
merr,
|
|
||||||
exec.Run(ctx, "kpartx", "-d", b.loDevice),
|
|
||||||
exec.Run(ctx, "losetup", "-d", b.loDevice),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) copyRootFS(ctx context.Context) error {
|
func (b *builder) copyRootFS(ctx context.Context) error {
|
||||||
@ -375,37 +297,14 @@ func (b *builder) copyRootFS(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func diskUUID(ctx context.Context, disk string) (string, error) {
|
func (b *builder) setupRootFS(ctx context.Context) error {
|
||||||
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", disk)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return strings.TrimSuffix(o, "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) setupRootFS(ctx context.Context) (err error) {
|
|
||||||
logrus.Infof("setting up rootfs")
|
logrus.Infof("setting up rootfs")
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var fstab string
|
b.diskUUD = strings.TrimSuffix(o, "\n")
|
||||||
if b.splitBoot {
|
fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD)
|
||||||
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 {
|
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -425,46 +324,44 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
|
|||||||
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
|
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if b.osRelease.ID != ReleaseAlpine {
|
||||||
switch b.osRelease.ID {
|
|
||||||
case ReleaseAlpine:
|
|
||||||
by, err := os.ReadFile(b.chPath("/etc/inittab"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
|
|
||||||
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
by, err := os.ReadFile(b.chPath("/etc/inittab"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
|
||||||
|
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) cmdline(_ context.Context) string {
|
func (b *builder) installKernel(ctx context.Context) error {
|
||||||
if !b.isLuksEnabled() {
|
logrus.Infof("installing linux kernel")
|
||||||
return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra)
|
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
switch b.osRelease.ID {
|
sysconfig, err := sysconfig(b.osRelease)
|
||||||
case ReleaseAlpine:
|
if err != nil {
|
||||||
return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra)
|
return err
|
||||||
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)
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD, b.cmdLineExtra), perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) installBootloader(ctx context.Context) error {
|
func (b *builder) setupMBR(ctx context.Context) error {
|
||||||
logrus.Infof("installing bootloader")
|
logrus.Infof("writing MBR")
|
||||||
return b.bootloader.Setup(ctx, b.loDevice, b.mntPoint, b.cmdline(ctx))
|
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 {
|
func (b *builder) convert2Img(ctx context.Context) error {
|
||||||
@ -487,30 +384,22 @@ func (b *builder) chPath(path string) string {
|
|||||||
return fmt.Sprintf("%s%s", b.mntPoint, path)
|
return fmt.Sprintf("%s%s", b.mntPoint, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) isLuksEnabled() bool {
|
|
||||||
return b.luksPassword != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) Close() error {
|
func (b *builder) Close() error {
|
||||||
return b.img.Close()
|
return b.img.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func block(path string, size uint64) error {
|
func block(path string, size int64) error {
|
||||||
f, err := os.Create(path)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
return f.Truncate(int64(size))
|
return f.Truncate(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) checkDependencies() error {
|
func checkDependencies() error {
|
||||||
var merr error
|
var merr error
|
||||||
deps := []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "dd", "mkfs.ext4", "cryptsetup"}
|
for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "partprobe", "qemu-img", "extlinux", "dd", "mkfs"} {
|
||||||
if _, ok := b.bootloader.(*syslinux); ok {
|
|
||||||
deps = append(deps, "extlinux")
|
|
||||||
}
|
|
||||||
for _, v := range deps {
|
|
||||||
if _, err := exec2.LookPath(v); err != nil {
|
if _, err := exec2.LookPath(v); err != nil {
|
||||||
merr = multierr.Append(merr, err)
|
merr = multierr.Append(merr, err)
|
||||||
}
|
}
|
||||||
@ -521,10 +410,3 @@ func (b *builder) checkDependencies() error {
|
|||||||
func OutputFormats() []string {
|
func OutputFormats() []string {
|
||||||
return formats[:]
|
return formats[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifElse(v bool, t string, f string) string {
|
|
||||||
if v {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
148
builder_test.go
Normal file
148
builder_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package d2vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.linka.cloud/d2vm/pkg/docker"
|
||||||
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, initrd string) {
|
||||||
|
require.NoError(t, docker.Pull(ctx, img))
|
||||||
|
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(img))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpPath, 0755))
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
logrus.Infof("inspecting image %s", img)
|
||||||
|
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer docker.Remove(ctx, img)
|
||||||
|
sys, err := sysconfig(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, sysconf, sys)
|
||||||
|
d, err := NewDockerfile(r, img, "root", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
logrus.Infof("docker image based on %s", d.Release.Name)
|
||||||
|
p := filepath.Join(tmpPath, docker.FormatImgName(img))
|
||||||
|
dir := filepath.Dir(p)
|
||||||
|
f, err := os.Create(p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
require.NoError(t, d.Render(f))
|
||||||
|
imgUUID := uuid.New().String()
|
||||||
|
logrus.Infof("building kernel enabled image")
|
||||||
|
require.NoError(t, docker.Build(ctx, imgUUID, p, dir))
|
||||||
|
defer docker.Remove(ctx, imgUUID)
|
||||||
|
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", kernel))
|
||||||
|
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", initrd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyslinuxCfg(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
image string
|
||||||
|
kernel string
|
||||||
|
initrd string
|
||||||
|
sysconfig string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
image: "ubuntu:18.04",
|
||||||
|
kernel: "/vmlinuz",
|
||||||
|
initrd: "/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:20.04",
|
||||||
|
kernel: "/boot/vmlinuz",
|
||||||
|
initrd: "/boot/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:22.04",
|
||||||
|
kernel: "/boot/vmlinuz",
|
||||||
|
initrd: "/boot/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "ubuntu:latest",
|
||||||
|
kernel: "/boot/vmlinuz",
|
||||||
|
initrd: "/boot/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgUbuntu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:9",
|
||||||
|
kernel: "/vmlinuz",
|
||||||
|
initrd: "/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:10",
|
||||||
|
kernel: "/vmlinuz",
|
||||||
|
initrd: "/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:11",
|
||||||
|
kernel: "/vmlinuz",
|
||||||
|
initrd: "/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "debian:latest",
|
||||||
|
kernel: "/vmlinuz",
|
||||||
|
initrd: "/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgDebian,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "alpine",
|
||||||
|
kernel: "/boot/vmlinuz-virt",
|
||||||
|
initrd: "/boot/initramfs-virt",
|
||||||
|
sysconfig: syslinuxCfgAlpine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "centos:8",
|
||||||
|
kernel: "/boot/vmlinuz",
|
||||||
|
initrd: "/boot/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgCentOS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "centos:latest",
|
||||||
|
kernel: "/boot/vmlinuz",
|
||||||
|
initrd: "/boot/initrd.img",
|
||||||
|
sysconfig: syslinuxCfgCentOS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
exec.SetDebug(true)
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.image, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
testSysconfig(t, ctx, test.image, test.sysconfig, test.kernel, test.initrd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -30,16 +30,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file = "Dockerfile"
|
file = "Dockerfile"
|
||||||
tag = "d2vm-" + uuid.New().String()
|
tag = "d2vm-" + uuid.New().String()
|
||||||
buildArgs []string
|
networkManager string
|
||||||
buildCmd = &cobra.Command{
|
buildArgs []string
|
||||||
|
buildCmd = &cobra.Command{
|
||||||
Use: "build [context directory]",
|
Use: "build [context directory]",
|
||||||
Short: "Build a vm image from Dockerfile",
|
Short: "Build a vm image from Dockerfile",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// TODO(adphi): resolve context path
|
// TODO(adphi): resolve context path
|
||||||
if runtime.GOOS != "linux" || !isRoot() {
|
if runtime.GOOS != "linux" {
|
||||||
ctxAbsPath, err := filepath.Abs(args[0])
|
ctxAbsPath, err := filepath.Abs(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -79,9 +80,6 @@ var (
|
|||||||
}
|
}
|
||||||
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...)
|
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...)
|
||||||
}
|
}
|
||||||
if err := validateFlags(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -89,11 +87,16 @@ var (
|
|||||||
if file == "" {
|
if file == "" {
|
||||||
file = filepath.Join(args[0], "Dockerfile")
|
file = filepath.Join(args[0], "Dockerfile")
|
||||||
}
|
}
|
||||||
|
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
||||||
|
if !force {
|
||||||
|
return fmt.Errorf("%s already exists", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
logrus.Infof("building docker image from %s", file)
|
logrus.Infof("building docker image from %s", file)
|
||||||
if err := docker.Build(cmd.Context(), pull, tag, file, args[0], platform, buildArgs...); err != nil {
|
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := d2vm.Convert(
|
return d2vm.Convert(
|
||||||
cmd.Context(),
|
cmd.Context(),
|
||||||
tag,
|
tag,
|
||||||
d2vm.WithSize(size),
|
d2vm.WithSize(size),
|
||||||
@ -101,24 +104,8 @@ var (
|
|||||||
d2vm.WithOutput(output),
|
d2vm.WithOutput(output),
|
||||||
d2vm.WithCmdLineExtra(cmdLineExtra),
|
d2vm.WithCmdLineExtra(cmdLineExtra),
|
||||||
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
||||||
d2vm.WithBootLoader(bootloader),
|
|
||||||
d2vm.WithRaw(raw),
|
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())
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -129,5 +116,11 @@ func init() {
|
|||||||
buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile")
|
buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile")
|
||||||
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
|
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
|
||||||
|
|
||||||
buildCmd.Flags().AddFlagSet(buildFlags())
|
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
|
buildCmd.Flags().StringVarP(&password, "password", "p", "", "Optional root user password")
|
||||||
|
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
|
buildCmd.Flags().BoolVar(&force, "force", false, "Override output image")
|
||||||
|
buildCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
|
||||||
|
buildCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
|
||||||
|
buildCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -15,9 +15,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -28,13 +30,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
raw bool
|
||||||
|
pull = false
|
||||||
|
cmdLineExtra = ""
|
||||||
|
|
||||||
convertCmd = &cobra.Command{
|
convertCmd = &cobra.Command{
|
||||||
Use: "convert [docker image]",
|
Use: "convert [docker image]",
|
||||||
Short: "Convert Docker image to vm image",
|
Short: "Convert Docker image to vm image",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if runtime.GOOS != "linux" || !isRoot() {
|
if runtime.GOOS != "linux" {
|
||||||
abs, err := filepath.Abs(output)
|
abs, err := filepath.Abs(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -49,32 +55,38 @@ var (
|
|||||||
}
|
}
|
||||||
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...)
|
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...)
|
||||||
}
|
}
|
||||||
if err := validateFlags(); err != nil {
|
img := args[0]
|
||||||
return err
|
tag := "latest"
|
||||||
|
if parts := strings.Split(img, ":"); len(parts) > 1 {
|
||||||
|
img, tag = parts[0], parts[1]
|
||||||
}
|
}
|
||||||
size, err := parseSize(size)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
img := args[0]
|
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
||||||
|
if !force {
|
||||||
|
return fmt.Errorf("%s already exists", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
found := false
|
found := false
|
||||||
if !pull {
|
if !pull {
|
||||||
imgs, err := docker.ImageList(cmd.Context(), img)
|
imgs, err := docker.ImageList(cmd.Context(), img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
found = len(imgs) == 1 && imgs[0] == img
|
found = len(imgs) == 1 && imgs[0] == fmt.Sprintf("%s:%s", img, tag)
|
||||||
if found {
|
if found {
|
||||||
logrus.Infof("using local image %s", img)
|
logrus.Infof("using local image %s:%s", img, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pull || !found {
|
if pull || !found {
|
||||||
logrus.Infof("pulling image %s", img)
|
logrus.Infof("pulling image %s", img)
|
||||||
if err := docker.Pull(cmd.Context(), platform, img); err != nil {
|
if err := docker.Pull(cmd.Context(), img); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := d2vm.Convert(
|
return d2vm.Convert(
|
||||||
cmd.Context(),
|
cmd.Context(),
|
||||||
img,
|
img,
|
||||||
d2vm.WithSize(size),
|
d2vm.WithSize(size),
|
||||||
@ -82,38 +94,28 @@ var (
|
|||||||
d2vm.WithOutput(output),
|
d2vm.WithOutput(output),
|
||||||
d2vm.WithCmdLineExtra(cmdLineExtra),
|
d2vm.WithCmdLineExtra(cmdLineExtra),
|
||||||
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
|
||||||
d2vm.WithBootLoader(bootloader),
|
|
||||||
d2vm.WithRaw(raw),
|
d2vm.WithRaw(raw),
|
||||||
d2vm.WithSplitBoot(splitBoot),
|
)
|
||||||
d2vm.WithBootSize(bootSize),
|
|
||||||
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
|
|
||||||
d2vm.WithLuksPassword(luksPassword),
|
|
||||||
d2vm.WithKeepCache(keepCache),
|
|
||||||
d2vm.WithPlatform(platform),
|
|
||||||
d2vm.WithPull(pull),
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// set user permissions on the output file if the command was run with sudo
|
|
||||||
if uid, ok := sudoUser(); ok {
|
|
||||||
if err := os.Chown(output, uid, uid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maybeMakeContainerDisk(cmd.Context())
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseSize(s string) (uint64, error) {
|
func parseSize(s string) (int64, error) {
|
||||||
var v datasize.ByteSize
|
var v datasize.ByteSize
|
||||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
if err := v.UnmarshalText([]byte(s)); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return uint64(v), nil
|
return int64(v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
convertCmd.Flags().AddFlagSet(buildFlags())
|
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
|
||||||
|
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
|
convertCmd.Flags().StringVarP(&password, "password", "p", "", "Optional root user password")
|
||||||
|
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
|
convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image")
|
||||||
|
convertCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one")
|
||||||
|
convertCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown")
|
||||||
|
convertCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more")
|
||||||
rootCmd.AddCommand(convertCmd)
|
rootCmd.AddCommand(convertCmd)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -20,8 +20,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -34,6 +32,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
output = "disk0.qcow2"
|
||||||
|
size = "1G"
|
||||||
|
password = ""
|
||||||
|
force = false
|
||||||
verbose = false
|
verbose = false
|
||||||
timeFormat = ""
|
timeFormat = ""
|
||||||
format = "qcow2"
|
format = "qcow2"
|
||||||
@ -75,16 +77,14 @@ func main() {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
rootCmd.ExecuteContext(ctx)
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "debug", "d", false, "Enable Debug output")
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "debug", "d", false, "Enable Debug output")
|
||||||
rootCmd.PersistentFlags().MarkDeprecated("debug", "use -v instead")
|
rootCmd.PersistentFlags().MarkDeprecated("debug", "use -v instead")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable Verbose output")
|
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)'")
|
rootCmd.PersistentFlags().StringVarP(&timeFormat, "time", "t", "none", "Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)'")
|
||||||
color.NoColor = false
|
color.NoColor = false
|
||||||
logrus.StandardLogger().Formatter = &logfmtFormatter{start: time.Now()}
|
logrus.StandardLogger().Formatter = &logfmtFormatter{start: time.Now()}
|
||||||
}
|
}
|
||||||
@ -133,25 +133,3 @@ func (f *logfmtFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return b.Bytes(), nil
|
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
|
|
||||||
}
|
|
||||||
|
@ -24,7 +24,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -35,14 +34,13 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/svenwiltink/sparsecat"
|
"github.com/svenwiltink/sparsecat"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/qemu_img"
|
exec2 "go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hetznerTokenEnv = "HETZNER_TOKEN"
|
hetznerTokenEnv = "HETZNER_TOKEN"
|
||||||
serverImg = "ubuntu-20.04"
|
serverImg = "ubuntu-20.04"
|
||||||
vmBlock = "sda"
|
vmBlockPath = "/dev/sda"
|
||||||
vmBlockPath = "/dev/" + vmBlock
|
|
||||||
sparsecatPath = "/usr/local/bin/sparsecat"
|
sparsecatPath = "/usr/local/bin/sparsecat"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -70,8 +68,6 @@ func init() {
|
|||||||
HetznerCmd.Flags().StringVarP(&hetznerSSHKeyPath, "ssh-key", "i", "", "d2vm image identity key")
|
HetznerCmd.Flags().StringVarP(&hetznerSSHKeyPath, "ssh-key", "i", "", "d2vm image identity key")
|
||||||
HetznerCmd.Flags().BoolVar(&hetznerRemove, "rm", false, "remove server when done")
|
HetznerCmd.Flags().BoolVar(&hetznerRemove, "rm", false, "remove server when done")
|
||||||
HetznerCmd.Flags().StringVarP(&hetznerServerName, "name", "n", "d2vm", "d2vm server name")
|
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) {
|
func Hetzner(cmd *cobra.Command, args []string) {
|
||||||
@ -83,7 +79,7 @@ func Hetzner(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.Writer, stdout io.Writer) error {
|
func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.Writer, stdout io.Writer) error {
|
||||||
i, err := qemu_img.Info(ctx, imgPath)
|
i, err := ImgInfo(ctx, imgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -95,11 +91,11 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(rawPath)
|
defer os.RemoveAll(rawPath)
|
||||||
logrus.Infof("converting image to raw: %s", rawPath)
|
logrus.Infof("converting image to raw: %s", rawPath)
|
||||||
if err := qemu_img.Convert(ctx, "raw", imgPath, rawPath); err != nil {
|
if err := exec2.Run(ctx, "qemu-img", "convert", "-O", "raw", imgPath, rawPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
imgPath = rawPath
|
imgPath = rawPath
|
||||||
i, err = qemu_img.Info(ctx, imgPath)
|
i, err = ImgInfo(ctx, imgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -115,23 +111,10 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
arch := "amd64"
|
img, _, err := c.Image.GetByName(ctx, serverImg)
|
||||||
harch := hcloud.ArchitectureX86
|
|
||||||
if strings.HasPrefix(strings.ToLower(hetznerVMType), "cax") {
|
|
||||||
harch = hcloud.ArchitectureARM
|
|
||||||
arch = "arm64"
|
|
||||||
}
|
|
||||||
sparsecatBin, err := Sparsecat(arch)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
l, _, err := c.Location.Get(ctx, hetznerDatacenter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -140,9 +123,9 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
sres, _, err := c.Server.Create(ctx, hcloud.ServerCreateOpts{
|
sres, _, err := c.Server.Create(ctx, hcloud.ServerCreateOpts{
|
||||||
Name: hetznerServerName,
|
Name: hetznerServerName,
|
||||||
ServerType: st,
|
ServerType: st,
|
||||||
Image: imgs[0],
|
Image: img,
|
||||||
Location: l,
|
Location: l,
|
||||||
StartAfterCreate: hcloud.Ptr(false),
|
StartAfterCreate: hcloud.Bool(false),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -201,7 +184,7 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
if _, err := io.Copy(f, bytes.NewReader(sparsecatBin)); err != nil {
|
if _, err := io.Copy(f, bytes.NewReader(sparsecatBinary)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := f.Close(); err != nil {
|
if err := f.Close(); err != nil {
|
||||||
@ -285,31 +268,13 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
return ctx.Err()
|
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()
|
gses, err := sc.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer gses.Close()
|
defer gses.Close()
|
||||||
logrus.Infof("resizing disk partition")
|
logrus.Infof("resizing disk partition")
|
||||||
cmd = fmt.Sprintf("growpart %s %d", vmBlockPath, vmPartNumber)
|
cmd := fmt.Sprintf("growpart %s 1", vmBlockPath)
|
||||||
logrus.Debugf("$ %s", cmd)
|
logrus.Debugf("$ %s", cmd)
|
||||||
if b, err := gses.CombinedOutput(cmd); err != nil {
|
if b, err := gses.CombinedOutput(cmd); err != nil {
|
||||||
return fmt.Errorf("%v: %s", err, string(b))
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
@ -322,7 +287,7 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
}
|
}
|
||||||
defer cses.Close()
|
defer cses.Close()
|
||||||
logrus.Infof("checking disk partition")
|
logrus.Infof("checking disk partition")
|
||||||
cmd = fmt.Sprintf("e2fsck -yf %s%d", vmBlockPath, vmPartNumber)
|
cmd = fmt.Sprintf("e2fsck -yf %s1", vmBlockPath)
|
||||||
logrus.Debugf("$ %s", cmd)
|
logrus.Debugf("$ %s", cmd)
|
||||||
if b, err := cses.CombinedOutput(cmd); err != nil {
|
if b, err := cses.CombinedOutput(cmd); err != nil {
|
||||||
return fmt.Errorf("%v: %s", err, string(b))
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
@ -335,7 +300,7 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
|
|||||||
}
|
}
|
||||||
defer eses.Close()
|
defer eses.Close()
|
||||||
logrus.Infof("extending partition file system")
|
logrus.Infof("extending partition file system")
|
||||||
cmd = fmt.Sprintf("resize2fs %s%d", vmBlockPath, vmPartNumber)
|
cmd = fmt.Sprintf("resize2fs %s1", vmBlockPath)
|
||||||
logrus.Debugf("$ %s", cmd)
|
logrus.Debugf("$ %s", cmd)
|
||||||
if b, err := eses.CombinedOutput(cmd); err != nil {
|
if b, err := eses.CombinedOutput(cmd); err != nil {
|
||||||
return fmt.Errorf("%v: %s", err, string(b))
|
return fmt.Errorf("%v: %s", err, string(b))
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/qemu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -28,11 +33,12 @@ var (
|
|||||||
arch string
|
arch string
|
||||||
cpus uint
|
cpus uint
|
||||||
mem uint
|
mem uint
|
||||||
bios string
|
|
||||||
qemuCmd string
|
qemuCmd string
|
||||||
qemuDetached bool
|
qemuDetached bool
|
||||||
networking string
|
networking string
|
||||||
publishFlags MultipleFlag
|
publishFlags MultipleFlag
|
||||||
|
deviceFlags MultipleFlag
|
||||||
|
usbEnabled bool
|
||||||
|
|
||||||
QemuCmd = &cobra.Command{
|
QemuCmd = &cobra.Command{
|
||||||
Use: "qemu [options] [image-path]",
|
Use: "qemu [options] [image-path]",
|
||||||
@ -65,6 +71,7 @@ func init() {
|
|||||||
|
|
||||||
// Paths and settings for disks
|
// Paths and settings for disks
|
||||||
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
|
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
|
||||||
|
flags.StringVar(&data, "data", "", "String of metadata to pass to VM")
|
||||||
|
|
||||||
// VM configuration
|
// VM configuration
|
||||||
flags.StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
|
flags.StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
|
||||||
@ -72,8 +79,6 @@ func init() {
|
|||||||
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
|
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
|
||||||
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
|
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
|
||||||
|
|
||||||
flags.StringVar(&bios, "bios", "", "Path to the optional bios binary")
|
|
||||||
|
|
||||||
// Backend configuration
|
// Backend configuration
|
||||||
flags.StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
|
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")
|
flags.BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background")
|
||||||
@ -82,46 +87,362 @@ func init() {
|
|||||||
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.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 [])")
|
flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])")
|
||||||
|
|
||||||
|
// USB devices
|
||||||
|
flags.BoolVar(&usbEnabled, "usb", false, "Enable USB controller")
|
||||||
|
|
||||||
|
flags.Var(&deviceFlags, "device", "Add USB host device(s). Format driver[,prop=value][,...] -- add device, like --device on the qemu command line.")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Qemu(cmd *cobra.Command, args []string) {
|
func Qemu(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
|
||||||
|
vmUUID := uuid.New()
|
||||||
|
// These envvars override the corresponding command line
|
||||||
|
// options. So this must remain after the `flags.Parse` above.
|
||||||
|
accel = GetStringValue("LINUXKIT_QEMU_ACCEL", accel, "")
|
||||||
|
|
||||||
path := args[0]
|
path := args[0]
|
||||||
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
var publishedPorts []PublishedPort
|
|
||||||
|
for i, d := range disks {
|
||||||
|
id := ""
|
||||||
|
if i != 0 {
|
||||||
|
id = strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
if d.Size != 0 && d.Format == "" {
|
||||||
|
d.Format = "qcow2"
|
||||||
|
}
|
||||||
|
if d.Size != 0 && d.Path == "" {
|
||||||
|
d.Path = "disk" + id + ".img"
|
||||||
|
}
|
||||||
|
if d.Path == "" {
|
||||||
|
log.Fatalf("disk specified with no size or name")
|
||||||
|
}
|
||||||
|
disks[i] = d
|
||||||
|
}
|
||||||
|
|
||||||
|
disks = append(Disks{DiskConfig{Path: path}}, disks...)
|
||||||
|
|
||||||
|
if networking == "" || networking == "default" {
|
||||||
|
dflt := qemuNetworkingDefault
|
||||||
|
networking = dflt
|
||||||
|
}
|
||||||
|
netMode := strings.SplitN(networking, ",", 2)
|
||||||
|
|
||||||
|
var netdevConfig string
|
||||||
|
switch netMode[0] {
|
||||||
|
case qemuNetworkingUser:
|
||||||
|
netdevConfig = "user,id=t0"
|
||||||
|
case qemuNetworkingTap:
|
||||||
|
if len(netMode) != 2 {
|
||||||
|
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingTap)
|
||||||
|
}
|
||||||
|
if len(publishFlags) != 0 {
|
||||||
|
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||||
|
}
|
||||||
|
netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
|
||||||
|
case qemuNetworkingBridge:
|
||||||
|
if len(netMode) != 2 {
|
||||||
|
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingBridge)
|
||||||
|
}
|
||||||
|
if len(publishFlags) != 0 {
|
||||||
|
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||||
|
}
|
||||||
|
netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
|
||||||
|
case qemuNetworkingNone:
|
||||||
|
if len(publishFlags) != 0 {
|
||||||
|
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||||
|
}
|
||||||
|
netdevConfig = ""
|
||||||
|
default:
|
||||||
|
log.Fatalf("Invalid networking mode: %s", netMode[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
config := QemuConfig{
|
||||||
|
Path: path,
|
||||||
|
GUI: enableGUI,
|
||||||
|
Disks: disks,
|
||||||
|
Arch: arch,
|
||||||
|
CPUs: cpus,
|
||||||
|
Memory: mem,
|
||||||
|
Accel: accel,
|
||||||
|
Detached: qemuDetached,
|
||||||
|
QemuBinPath: qemuCmd,
|
||||||
|
PublishedPorts: publishFlags,
|
||||||
|
NetdevConfig: netdevConfig,
|
||||||
|
UUID: vmUUID,
|
||||||
|
USB: usbEnabled,
|
||||||
|
Devices: deviceFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := discoverBinaries(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = runQemuLocal(config); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQemuLocal(config QemuConfig) error {
|
||||||
|
var args []string
|
||||||
|
config, args = buildQemuCmdline(config)
|
||||||
|
|
||||||
|
for _, d := range config.Disks {
|
||||||
|
// If disk doesn't exist then create one
|
||||||
|
if _, err := os.Stat(d.Path); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Debugf("Creating new qemu disk [%s] format %s", d.Path, d.Format)
|
||||||
|
qemuImgCmd := exec.Command(config.QemuImgPath, "create", "-f", d.Format, d.Path, fmt.Sprintf("%dM", d.Size))
|
||||||
|
log.Debugf("%v", qemuImgCmd.Args)
|
||||||
|
if err := qemuImgCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("Error creating disk [%s] format %s: %s", d.Path, d.Format, err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Using existing disk [%s] format %s", d.Path, d.Format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detached mode is only supported in a container.
|
||||||
|
if config.Detached == true {
|
||||||
|
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuCmd := exec.Command(config.QemuBinPath, args...)
|
||||||
|
// If verbosity is enabled print out the full path/arguments
|
||||||
|
log.Debugf("%v", qemuCmd.Args)
|
||||||
|
|
||||||
|
// If we're not using a separate window then link the execution to stdin/out
|
||||||
|
if config.GUI != true {
|
||||||
|
qemuCmd.Stdin = os.Stdin
|
||||||
|
qemuCmd.Stdout = os.Stdout
|
||||||
|
qemuCmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return qemuCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQemuCmdline(config QemuConfig) (QemuConfig, []string) {
|
||||||
|
// Iterate through the flags and build arguments
|
||||||
|
var qemuArgs []string
|
||||||
|
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", config.CPUs))
|
||||||
|
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", config.Memory))
|
||||||
|
qemuArgs = append(qemuArgs, "-uuid", config.UUID.String())
|
||||||
|
|
||||||
|
// Need to specify the vcpu type when running qemu on arm64 platform, for security reason,
|
||||||
|
// the vcpu should be "host" instead of other names such as "cortex-a53"...
|
||||||
|
if config.Arch == "aarch64" {
|
||||||
|
if runtime.GOARCH == "arm64" {
|
||||||
|
qemuArgs = append(qemuArgs, "-cpu", "host")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// goArch is the GOARCH equivalent of config.Arch
|
||||||
|
var goArch string
|
||||||
|
switch config.Arch {
|
||||||
|
case "s390x":
|
||||||
|
goArch = "s390x"
|
||||||
|
case "aarch64":
|
||||||
|
goArch = "arm64"
|
||||||
|
case "x86_64":
|
||||||
|
goArch = "amd64"
|
||||||
|
default:
|
||||||
|
log.Fatalf("%s is an unsupported architecture.", config.Arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if goArch != runtime.GOARCH {
|
||||||
|
log.Infof("Disable acceleration as %s != %s", config.Arch, runtime.GOARCH)
|
||||||
|
config.Accel = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Accel != "" {
|
||||||
|
switch config.Arch {
|
||||||
|
case "s390x":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", config.Accel))
|
||||||
|
case "aarch64":
|
||||||
|
gic := ""
|
||||||
|
// VCPU supports less PA bits (36) than requested by the memory map (40)
|
||||||
|
highmem := "highmem=off,"
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// gic-version=host requires KVM, which implies Linux
|
||||||
|
gic = "gic_version=host,"
|
||||||
|
highmem = ""
|
||||||
|
}
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("virt,%s%saccel=%s", gic, highmem, config.Accel))
|
||||||
|
default:
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", config.Accel))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch config.Arch {
|
||||||
|
case "s390x":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "s390-ccw-virtio")
|
||||||
|
case "aarch64":
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "virt")
|
||||||
|
default:
|
||||||
|
qemuArgs = append(qemuArgs, "-machine", "q35")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rng-random does not work on macOS
|
||||||
|
// Temporarily disable it until fixed upstream.
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
rng := "rng-random,id=rng0"
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
rng = rng + ",filename=/dev/urandom"
|
||||||
|
}
|
||||||
|
if config.Arch == "s390x" {
|
||||||
|
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-ccw,rng=rng0")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-pci,rng=rng0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastDisk int
|
||||||
|
for i, d := range config.Disks {
|
||||||
|
index := i
|
||||||
|
if d.Format != "" {
|
||||||
|
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",format="+d.Format+",index="+strconv.Itoa(index)+",media=disk")
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",index="+strconv.Itoa(index)+",media=disk")
|
||||||
|
}
|
||||||
|
lastDisk = index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CDROMs start from at least hdc
|
||||||
|
if lastDisk < 2 {
|
||||||
|
lastDisk = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.NetdevConfig == "" {
|
||||||
|
qemuArgs = append(qemuArgs, "-net", "none")
|
||||||
|
} else {
|
||||||
|
mac := generateMAC()
|
||||||
|
if config.Arch == "s390x" {
|
||||||
|
qemuArgs = append(qemuArgs, "-device", "virtio-net-ccw,netdev=t0,mac="+mac.String())
|
||||||
|
} else {
|
||||||
|
qemuArgs = append(qemuArgs, "-device", "virtio-net-pci,netdev=t0,mac="+mac.String())
|
||||||
|
}
|
||||||
|
forwardings, err := buildQemuForwardings(config.PublishedPorts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
qemuArgs = append(qemuArgs, "-netdev", config.NetdevConfig+forwardings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.GUI != true {
|
||||||
|
qemuArgs = append(qemuArgs, "-nographic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.USB == true {
|
||||||
|
qemuArgs = append(qemuArgs, "-usb")
|
||||||
|
}
|
||||||
|
for _, d := range config.Devices {
|
||||||
|
qemuArgs = append(qemuArgs, "-device", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, qemuArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverBinaries(config QemuConfig) (QemuConfig, error) {
|
||||||
|
if config.QemuImgPath != "" {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuBinPath := "qemu-system-" + config.Arch
|
||||||
|
qemuImgPath := "qemu-img"
|
||||||
|
|
||||||
|
var err error
|
||||||
|
config.QemuBinPath, err = exec.LookPath(qemuBinPath)
|
||||||
|
if err != nil {
|
||||||
|
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.QemuImgPath, err = exec.LookPath(qemuImgPath)
|
||||||
|
if err != nil {
|
||||||
|
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQemuForwardings(publishFlags MultipleFlag) (string, error) {
|
||||||
|
if len(publishFlags) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
var forwardings string
|
||||||
for _, publish := range publishFlags {
|
for _, publish := range publishFlags {
|
||||||
p, err := NewPublishedPort(publish)
|
p, err := NewPublishedPort(publish)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return "", err
|
||||||
}
|
}
|
||||||
publishedPorts = append(publishedPorts, p)
|
|
||||||
|
hostPort := p.Host
|
||||||
|
guestPort := p.Guest
|
||||||
|
|
||||||
|
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
|
||||||
}
|
}
|
||||||
opts := []qemu.Option{
|
|
||||||
qemu.WithDisks(disks...),
|
return forwardings, nil
|
||||||
qemu.WithAccel(accel),
|
}
|
||||||
qemu.WithArch(arch),
|
|
||||||
qemu.WithCPUs(cpus),
|
func buildDockerForwardings(publishedPorts []string) ([]string, error) {
|
||||||
qemu.WithMemory(mem),
|
pmap := []string{}
|
||||||
qemu.WithNetworking(networking),
|
for _, port := range publishedPorts {
|
||||||
qemu.WithStdin(os.Stdin),
|
s, err := NewPublishedPort(port)
|
||||||
qemu.WithStdout(os.Stdout),
|
if err != nil {
|
||||||
qemu.WithStderr(os.Stderr),
|
return nil, err
|
||||||
qemu.WithBios(bios),
|
}
|
||||||
}
|
pmap = append(pmap, "-p", fmt.Sprintf("%d:%d/%s", s.Host, s.Guest, s.Protocol))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return pmap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QemuConfig contains the config for Qemu
|
||||||
|
type QemuConfig struct {
|
||||||
|
Path string
|
||||||
|
GUI bool
|
||||||
|
Disks Disks
|
||||||
|
FWPath string
|
||||||
|
Arch string
|
||||||
|
CPUs uint
|
||||||
|
Memory uint
|
||||||
|
Accel string
|
||||||
|
Detached bool
|
||||||
|
QemuBinPath string
|
||||||
|
QemuImgPath string
|
||||||
|
PublishedPorts []string
|
||||||
|
NetdevConfig string
|
||||||
|
UUID uuid.UUID
|
||||||
|
USB bool
|
||||||
|
Devices []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func haveKVM() bool {
|
func haveKVM() bool {
|
||||||
_, err := os.Stat("/dev/kvm")
|
_, err := os.Stat("/dev/kvm")
|
||||||
return !os.IsNotExist(err)
|
return !os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateMAC() net.HardwareAddr {
|
||||||
|
mac := make([]byte, 6)
|
||||||
|
n, err := rand.Read(mac)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Fatal("failed to generate random mac address")
|
||||||
|
}
|
||||||
|
if n != 6 {
|
||||||
|
log.WithError(err).Fatalf("generated %d bytes for random mac address", n)
|
||||||
|
}
|
||||||
|
mac[0] &^= 0x01 // Clear multicast bit
|
||||||
|
mac[0] |= 0x2 // Set locally administered bit
|
||||||
|
return net.HardwareAddr(mac)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
//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=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.
|
// Copyright 2022 Linka Cloud All rights reserved.
|
||||||
//
|
//
|
||||||
@ -19,36 +18,23 @@ package run
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/qemu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed sparsecat-linux-amd64
|
//go:embed sparsecat-linux-amd64
|
||||||
var sparsecatAmdBinary []byte
|
var sparsecatBinary []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
|
// Handle flags with multiple occurrences
|
||||||
type MultipleFlag []string
|
type MultipleFlag []string
|
||||||
@ -205,8 +191,15 @@ func ConvertMBtoGB(i int) int {
|
|||||||
return (i + (1024 - i%1024)) / 1024
|
return (i + (1024 - i%1024)) / 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiskConfig is the config for a disk
|
||||||
|
type DiskConfig struct {
|
||||||
|
Path string
|
||||||
|
Size int
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
// Disks is the type for a list of DiskConfig
|
// Disks is the type for a list of DiskConfig
|
||||||
type Disks []qemu.Disk
|
type Disks []DiskConfig
|
||||||
|
|
||||||
func (l *Disks) String() string {
|
func (l *Disks) String() string {
|
||||||
return fmt.Sprint(*l)
|
return fmt.Sprint(*l)
|
||||||
@ -214,7 +207,7 @@ func (l *Disks) String() string {
|
|||||||
|
|
||||||
// Set is used by flag to configure value from CLI
|
// Set is used by flag to configure value from CLI
|
||||||
func (l *Disks) Set(value string) error {
|
func (l *Disks) Set(value string) error {
|
||||||
d := qemu.Disk{}
|
d := DiskConfig{}
|
||||||
s := strings.Split(value, ",")
|
s := strings.Split(value, ",")
|
||||||
for _, p := range s {
|
for _, p := range s {
|
||||||
c := strings.SplitN(p, "=", 2)
|
c := strings.SplitN(p, "=", 2)
|
||||||
@ -347,3 +340,23 @@ func (p *pw) Progress() int {
|
|||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
return p.total
|
return p.total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QemuInfo struct {
|
||||||
|
VirtualSize int `json:"virtual-size"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
ActualSize int `json:"actual-size"`
|
||||||
|
DirtyFlag bool `json:"dirty-flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImgInfo(ctx context.Context, path string) (*QemuInfo, error) {
|
||||||
|
o, err := exec.CommandContext(ctx, "qemu-img", "info", path, "--output", "json").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%v: %s", err, string(o))
|
||||||
|
}
|
||||||
|
var i QemuInfo
|
||||||
|
if err := json.Unmarshal(o, &i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &i, nil
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.linka.cloud/console"
|
"go.linka.cloud/console"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/qemu_img"
|
exec2 "go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -74,7 +74,7 @@ func vbox(ctx context.Context, path string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err)
|
return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err)
|
||||||
}
|
}
|
||||||
i, err := qemu_img.Info(ctx, path)
|
i, err := ImgInfo(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get image info: %v", err)
|
return fmt.Errorf("failed to get image info: %v", err)
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ func vbox(ctx context.Context, path string) error {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(vdi)
|
defer os.RemoveAll(vdi)
|
||||||
logrus.Infof("converting image to raw: %s", vdi)
|
logrus.Infof("converting image to raw: %s", vdi)
|
||||||
if err := qemu_img.Convert(ctx, "vdi", path, vdi); err != nil {
|
if err := exec2.Run(ctx, "qemu-img", "convert", "-O", "vdi", path, vdi); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path = vdi
|
path = vdi
|
||||||
|
89
config.go
89
config.go
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
161
config_test.go
161
config_test.go
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
21
convert.go
21
convert.go
@ -41,17 +41,12 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
|
|||||||
defer os.RemoveAll(tmpPath)
|
defer os.RemoveAll(tmpPath)
|
||||||
|
|
||||||
logrus.Infof("inspecting image %s", img)
|
logrus.Infof("inspecting image %s", img)
|
||||||
r, err := FetchDockerImageOSRelease(ctx, img)
|
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.luksPassword != "" && !r.SupportsLUKS() {
|
|
||||||
return fmt.Errorf("luks is not supported for %s %s", r.Name, r.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !o.raw {
|
if !o.raw {
|
||||||
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.hasGrubBIOS(), o.hasGrubEFI())
|
d, err := NewDockerfile(r, img, o.password, o.networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -67,20 +62,16 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logrus.Infof("building kernel enabled image")
|
logrus.Infof("building kernel enabled image")
|
||||||
if err := docker.Build(ctx, o.pull, imgUUID, p, dir, o.platform); err != nil {
|
if err := docker.Build(ctx, imgUUID, p, dir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !o.keepCache {
|
defer docker.Remove(ctx, imgUUID)
|
||||||
defer docker.Remove(ctx, imgUUID)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// for raw images, we just tag the image with the uuid
|
// for raw images, we just tag the image with the uuid
|
||||||
if err := docker.Tag(ctx, img, imgUUID); err != nil {
|
if err := docker.Tag(ctx, img, imgUUID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !o.keepCache {
|
defer docker.Remove(ctx, imgUUID)
|
||||||
defer docker.Remove(ctx, imgUUID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("creating vm image")
|
logrus.Infof("creating vm image")
|
||||||
@ -88,7 +79,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
|
|||||||
if format == "" {
|
if format == "" {
|
||||||
format = "raw"
|
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(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,34 +17,15 @@ package d2vm
|
|||||||
type ConvertOption func(o *convertOptions)
|
type ConvertOption func(o *convertOptions)
|
||||||
|
|
||||||
type convertOptions struct {
|
type convertOptions struct {
|
||||||
size uint64
|
size int64
|
||||||
password string
|
password string
|
||||||
output string
|
output string
|
||||||
cmdLineExtra string
|
cmdLineExtra string
|
||||||
networkManager NetworkManager
|
networkManager NetworkManager
|
||||||
bootLoader string
|
|
||||||
raw bool
|
raw bool
|
||||||
|
|
||||||
splitBoot bool
|
|
||||||
bootSize uint64
|
|
||||||
bootFS BootFS
|
|
||||||
|
|
||||||
luksPassword string
|
|
||||||
|
|
||||||
keepCache bool
|
|
||||||
platform string
|
|
||||||
pull bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *convertOptions) hasGrubBIOS() bool {
|
func WithSize(size int64) ConvertOption {
|
||||||
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) {
|
return func(o *convertOptions) {
|
||||||
o.size = size
|
o.size = size
|
||||||
}
|
}
|
||||||
@ -74,56 +55,8 @@ func WithNetworkManager(networkManager NetworkManager) ConvertOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithBootLoader(bootLoader string) ConvertOption {
|
|
||||||
return func(o *convertOptions) {
|
|
||||||
o.bootLoader = bootLoader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRaw(raw bool) ConvertOption {
|
func WithRaw(raw bool) ConvertOption {
|
||||||
return func(o *convertOptions) {
|
return func(o *convertOptions) {
|
||||||
o.raw = raw
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -24,11 +24,11 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/cmd/crane/cmd"
|
"github.com/google/go-containerregistry/cmd/crane/cmd"
|
||||||
"github.com/google/go-containerregistry/pkg/crane"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/daemon"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/docker"
|
|
||||||
"go.linka.cloud/d2vm/pkg/exec"
|
"go.linka.cloud/d2vm/pkg/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -81,18 +81,17 @@ func (i DockerImage) AsRunScript(w io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
|
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
|
||||||
if err := os.MkdirAll(imageTmpPath, os.ModePerm); err != nil {
|
ref, err := name.ParseReference(tag)
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
img, err := daemon.Image(ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(imageTmpPath, perm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
i := &image{
|
i := &image{
|
||||||
img: img,
|
img: img,
|
||||||
dir: imageTmpPath,
|
dir: imageTmpPath,
|
||||||
@ -110,9 +109,10 @@ type image struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i image) Flatten(ctx context.Context, out string) error {
|
func (i image) Flatten(ctx context.Context, out string) error {
|
||||||
if err := os.MkdirAll(out, os.ModePerm); err != nil {
|
if err := os.MkdirAll(out, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tar := filepath.Join(i.dir, "img.tar")
|
tar := filepath.Join(i.dir, "img.tar")
|
||||||
f, err := os.Create(tar)
|
f, err := os.Create(tar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -134,13 +134,13 @@ RUN rm -rf /etc/apk
|
|||||||
)
|
)
|
||||||
exec.SetDebug(true)
|
exec.SetDebug(true)
|
||||||
tmp := filepath.Join(os.TempDir(), "d2vm-tests", "image-flatten")
|
tmp := filepath.Join(os.TempDir(), "d2vm-tests", "image-flatten")
|
||||||
require.NoError(t, os.MkdirAll(tmp, os.ModePerm))
|
require.NoError(t, os.MkdirAll(tmp, perm))
|
||||||
defer os.RemoveAll(tmp)
|
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, "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, "resolv.conf"), []byte("nameserver 8.8.8.8"), perm))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
|
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
|
||||||
require.NoError(t, docker.Build(ctx, false, img, "", tmp, "linux/amd64"))
|
require.NoError(t, docker.Build(ctx, img, "", tmp))
|
||||||
defer docker.Remove(ctx, img)
|
defer docker.Remove(ctx, img)
|
||||||
|
|
||||||
imgTmp := filepath.Join(tmp, "image")
|
imgTmp := filepath.Join(tmp, "image")
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -37,10 +36,10 @@ var alpineDockerfile string
|
|||||||
var centOSDockerfile string
|
var centOSDockerfile string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile))
|
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Parse(ubuntuDockerfile))
|
||||||
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile))
|
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Parse(debianDockerfile))
|
||||||
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile))
|
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Parse(alpineDockerfile))
|
||||||
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile))
|
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Parse(centOSDockerfile))
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetworkManager string
|
type NetworkManager string
|
||||||
@ -65,37 +64,23 @@ type Dockerfile struct {
|
|||||||
Password string
|
Password string
|
||||||
Release OSRelease
|
Release OSRelease
|
||||||
NetworkManager NetworkManager
|
NetworkManager NetworkManager
|
||||||
Luks bool
|
|
||||||
GrubBIOS bool
|
|
||||||
GrubEFI bool
|
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Dockerfile) Grub() bool {
|
|
||||||
return d.GrubBIOS || d.GrubEFI
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Dockerfile) Render(w io.Writer) error {
|
func (d Dockerfile) Render(w io.Writer) error {
|
||||||
return d.tmpl.Execute(w, d)
|
return d.tmpl.Execute(w, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grubBIOS, grubEFI bool) (Dockerfile, error) {
|
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager) (Dockerfile, error) {
|
||||||
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, GrubBIOS: grubBIOS, GrubEFI: grubEFI}
|
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager}
|
||||||
var net NetworkManager
|
var net NetworkManager
|
||||||
switch release.ID {
|
switch release.ID {
|
||||||
case ReleaseDebian:
|
case ReleaseDebian:
|
||||||
d.tmpl = debianDockerfileTemplate
|
d.tmpl = debianDockerfileTemplate
|
||||||
net = NetworkManagerIfupdown2
|
net = NetworkManagerIfupdown2
|
||||||
case ReleaseKali:
|
|
||||||
d.tmpl = debianDockerfileTemplate
|
|
||||||
net = NetworkManagerIfupdown2
|
|
||||||
case ReleaseUbuntu:
|
case ReleaseUbuntu:
|
||||||
d.tmpl = ubuntuDockerfileTemplate
|
d.tmpl = ubuntuDockerfileTemplate
|
||||||
if release.VersionID < "18.04" {
|
net = NetworkManagerNetplan
|
||||||
net = NetworkManagerIfupdown2
|
|
||||||
} else {
|
|
||||||
net = NetworkManagerNetplan
|
|
||||||
}
|
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
d.tmpl = alpineDockerfileTemplate
|
d.tmpl = alpineDockerfileTemplate
|
||||||
net = NetworkManagerIfupdown2
|
net = NetworkManagerIfupdown2
|
||||||
@ -122,7 +107,3 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo
|
|||||||
}
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var tplFuncs = template.FuncMap{
|
|
||||||
"atoi": strconv.Atoi,
|
|
||||||
}
|
|
||||||
|
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
d2vm.linka.cloud
|
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
-h, --help help for d2vm
|
-h, --help help for d2vm
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -10,31 +10,21 @@ d2vm build [context directory] [flags]
|
|||||||
|
|
||||||
```
|
```
|
||||||
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
--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
|
--build-arg stringArray Set build-time variables
|
||||||
-f, --file string Name of the Dockerfile
|
-f, --file string Name of the Dockerfile
|
||||||
--force Override output qcow2 image
|
--force Override output image
|
||||||
-h, --help help for build
|
-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
|
--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")
|
-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
|
-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
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
-s, --size string The output image size (default "10G")
|
-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
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ See each sub-command's help for details on how to use the generated script.
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ To load completions for every new session, execute once:
|
|||||||
|
|
||||||
#### macOS:
|
#### macOS:
|
||||||
|
|
||||||
d2vm completion bash > $(brew --prefix)/etc/bash_completion.d/d2vm
|
d2vm completion bash > /usr/local/etc/bash_completion.d/d2vm
|
||||||
|
|
||||||
You will need to start a new shell for this setup to take effect.
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ d2vm completion bash
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ d2vm completion fish [flags]
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ d2vm completion powershell [flags]
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -11,10 +11,6 @@ to enable it. You can execute the following once:
|
|||||||
|
|
||||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
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:
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
#### Linux:
|
#### Linux:
|
||||||
@ -23,7 +19,7 @@ To load completions for every new session, execute once:
|
|||||||
|
|
||||||
#### macOS:
|
#### macOS:
|
||||||
|
|
||||||
d2vm completion zsh > $(brew --prefix)/share/zsh/site-functions/_d2vm
|
d2vm completion zsh > /usr/local/share/zsh/site-functions/_d2vm
|
||||||
|
|
||||||
You will need to start a new shell for this setup to take effect.
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
@ -42,7 +38,7 @@ d2vm completion zsh [flags]
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -10,29 +10,20 @@ d2vm convert [docker image] [flags]
|
|||||||
|
|
||||||
```
|
```
|
||||||
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
|
--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
|
-f, --force Override output qcow2 image
|
||||||
--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
|
-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
|
--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")
|
-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
|
-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
|
--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
|
--raw Just convert the container to virtual machine image without installing anything more
|
||||||
-s, --size string The output image size (default "10G")
|
-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
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ Run the virtual machine image
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -9,20 +9,18 @@ d2vm run hetzner [options] image-path [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
-h, --help help for hetzner
|
-h, --help help for hetzner
|
||||||
-l, --location string d2vm server location (default "hel1-dc2")
|
-n, --name string d2vm server name (default "d2vm")
|
||||||
-n, --name string d2vm server name (default "d2vm")
|
--rm remove server when done
|
||||||
--rm remove server when done
|
-i, --ssh-key string d2vm image identity key
|
||||||
-i, --ssh-key string d2vm image identity key
|
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
|
||||||
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
|
-u, --user string d2vm image ssh user (default "root")
|
||||||
-t, --type string d2vm server type (default "cx11")
|
|
||||||
-u, --user string d2vm image ssh user (default "root")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -9,11 +9,12 @@ d2vm run qemu [options] [image-path] [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "kvm:tcg")
|
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "hvf:tcg")
|
||||||
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
|
--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)
|
--cpus uint Number of CPUs (default 1)
|
||||||
|
--data string String of metadata to pass to VM
|
||||||
--detached Set qemu container to run in the background
|
--detached Set qemu container to run in the background
|
||||||
|
--device multiple-flag Add USB host device(s). Format driver[,prop=value][,...] -- add device, like --device on the qemu command line. (default A multiple flag is a type of flag that can be repeated any number of times)
|
||||||
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])
|
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])
|
||||||
--gui Set qemu to use video output instead of stdio
|
--gui Set qemu to use video output instead of stdio
|
||||||
-h, --help help for qemu
|
-h, --help help for qemu
|
||||||
@ -21,12 +22,13 @@ d2vm run qemu [options] [image-path] [flags]
|
|||||||
--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")
|
--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)
|
--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)
|
--qemu string Path to the qemu binary (otherwise look in $PATH)
|
||||||
|
--usb Enable USB controller
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ d2vm run vbox [options] image-path [flags]
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ d2vm version [flags]
|
|||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|
||||||
```
|
```
|
||||||
--time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
-t, --time string Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)' (default "none")
|
||||||
-v, --verbose Enable Verbose output
|
-v, --verbose Enable Verbose output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
212
e2e/e2e_test.go
212
e2e/e2e_test.go
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
58
fs.go
58
fs.go
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
87
go.mod
87
go.mod
@ -1,69 +1,70 @@
|
|||||||
module go.linka.cloud/d2vm
|
module go.linka.cloud/d2vm
|
||||||
|
|
||||||
go 1.20
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
github.com/google/go-containerregistry v0.14.0
|
github.com/google/go-containerregistry v0.8.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/hetznercloud/hcloud-go v1.50.0
|
github.com/hetznercloud/hcloud-go v1.35.2
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.4.0
|
||||||
github.com/pkg/sftp v1.10.1
|
github.com/pkg/sftp v1.10.1
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/stretchr/testify v1.8.4
|
|
||||||
github.com/svenwiltink/sparsecat v1.0.0
|
github.com/svenwiltink/sparsecat v1.0.0
|
||||||
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
|
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.8.0
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||||
golang.org/x/sys v0.10.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
github.com/containerd/containerd v1.5.8 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
|
||||||
github.com/creack/pty v1.1.15 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/cli v23.0.4+incompatible // indirect
|
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v23.0.4+incompatible // indirect
|
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/klauspost/compress v1.16.5 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.13.6 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/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-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
|
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.42.0 // indirect
|
github.com/prometheus/common v0.26.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||||
)
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||||
replace (
|
google.golang.org/grpc v1.43.0 // indirect
|
||||||
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.11.1
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
golang.org/x/crypto => golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
google.golang.org/protobuf => google.golang.org/protobuf v1.29.1
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
76
grub.go
76
grub.go
@ -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{})
|
|
||||||
}
|
|
65
grub_bios.go
65
grub_bios.go
@ -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{})
|
|
||||||
}
|
|
102
grub_common.go
102
grub_common.go
@ -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")
|
|
||||||
}
|
|
71
grub_efi.go
71
grub_efi.go
@ -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{})
|
|
||||||
}
|
|
@ -16,8 +16,11 @@ package d2vm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -31,7 +34,6 @@ const (
|
|||||||
ReleaseAlpine Release = "alpine"
|
ReleaseAlpine Release = "alpine"
|
||||||
ReleaseCentOS Release = "centos"
|
ReleaseCentOS Release = "centos"
|
||||||
ReleaseRHEL Release = "rhel"
|
ReleaseRHEL Release = "rhel"
|
||||||
ReleaseKali Release = "kali"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Release string
|
type Release string
|
||||||
@ -42,8 +44,6 @@ func (r Release) Supported() bool {
|
|||||||
return true
|
return true
|
||||||
case ReleaseDebian:
|
case ReleaseDebian:
|
||||||
return true
|
return true
|
||||||
case ReleaseKali:
|
|
||||||
return true
|
|
||||||
case ReleaseAlpine:
|
case ReleaseAlpine:
|
||||||
return true
|
return true
|
||||||
case ReleaseCentOS:
|
case ReleaseCentOS:
|
||||||
@ -63,31 +63,6 @@ type OSRelease struct {
|
|||||||
VersionCodeName string
|
VersionCodeName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r OSRelease) SupportsLUKS() bool {
|
|
||||||
switch r.ID {
|
|
||||||
case ReleaseUbuntu:
|
|
||||||
return r.VersionID >= "20.04"
|
|
||||||
case ReleaseDebian:
|
|
||||||
v, err := strconv.Atoi(r.VersionID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warnf("%s: failed to parse version id: %v", r.Version, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return v >= 10
|
|
||||||
case ReleaseKali:
|
|
||||||
// TODO: check version
|
|
||||||
return true
|
|
||||||
case ReleaseCentOS:
|
|
||||||
return true
|
|
||||||
case ReleaseAlpine:
|
|
||||||
return true
|
|
||||||
case ReleaseRHEL:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseOSRelease(s string) (OSRelease, error) {
|
func ParseOSRelease(s string) (OSRelease, error) {
|
||||||
env, err := godotenv.Parse(strings.NewReader(s))
|
env, err := godotenv.Parse(strings.NewReader(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -103,8 +78,40 @@ func ParseOSRelease(s string) (OSRelease, error) {
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchDockerImageOSRelease(ctx context.Context, img string) (OSRelease, error) {
|
const (
|
||||||
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", "--entrypoint", "cat", img, "/etc/os-release")
|
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, "osrelease.Dockerfile")
|
||||||
|
f, err := os.Create(d)
|
||||||
|
if err != nil {
|
||||||
|
return OSRelease{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := osReleaseDockerfileTemplate.Execute(f, img); err != nil {
|
||||||
|
return OSRelease{}, err
|
||||||
|
}
|
||||||
|
imgTag := fmt.Sprintf("os-release-%s", img)
|
||||||
|
if err := docker.Cmd(ctx, "image", "build", "-t", imgTag, "-f", d, tmpPath); err != nil {
|
||||||
|
return OSRelease{}, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := docker.Cmd(ctx, "image", "rm", imgTag); err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to cleanup OSRelease Docker Image")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", imgTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return OSRelease{}, err
|
return OSRelease{}, err
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -50,14 +50,11 @@ func CmdOut(ctx context.Context, args ...string) (string, string, error) {
|
|||||||
return exec.RunOut(ctx, "docker", args...)
|
return exec.RunOut(ctx, "docker", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Build(ctx context.Context, pull bool, tag, dockerfile, dir, platform string, buildArgs ...string) error {
|
func Build(ctx context.Context, tag, dockerfile, dir string, buildArgs ...string) error {
|
||||||
if dockerfile == "" {
|
if dockerfile == "" {
|
||||||
dockerfile = filepath.Join(dir, "Dockerfile")
|
dockerfile = filepath.Join(dir, "Dockerfile")
|
||||||
}
|
}
|
||||||
args := []string{"image", "build", "-t", tag, "-f", dockerfile, "--platform", platform}
|
args := []string{"image", "build", "-t", tag, "-f", dockerfile}
|
||||||
if pull {
|
|
||||||
args = append(args, "--pull")
|
|
||||||
}
|
|
||||||
for _, v := range buildArgs {
|
for _, v := range buildArgs {
|
||||||
args = append(args, "--build-arg", v)
|
args = append(args, "--build-arg", v)
|
||||||
}
|
}
|
||||||
@ -95,19 +92,12 @@ func ImageList(ctx context.Context, tag string) ([]string, error) {
|
|||||||
return imgs, s.Err()
|
return imgs, s.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ImageSave(ctx context.Context, tag, file string) error {
|
func Pull(ctx context.Context, tag string) error {
|
||||||
return Cmd(ctx, "image", "save", "-o", file, tag)
|
return Cmd(ctx, "image", "pull", 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 {
|
func RunInteractiveAndRemove(ctx context.Context, args ...string) error {
|
||||||
|
logrus.Tracef("running 'docker run --rm -i -t %s'", strings.Join(args, " "))
|
||||||
cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...)
|
cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@ -137,18 +127,8 @@ func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...s
|
|||||||
if version == "" {
|
if version == "" {
|
||||||
version = "latest"
|
version = "latest"
|
||||||
}
|
}
|
||||||
a := []string{"run", "--rm"}
|
a := []string{
|
||||||
|
|
||||||
interactive := isInteractive()
|
|
||||||
|
|
||||||
if interactive {
|
|
||||||
a = append(a, "-i", "-t")
|
|
||||||
}
|
|
||||||
a = append(a,
|
|
||||||
"--privileged",
|
"--privileged",
|
||||||
"-e",
|
|
||||||
// yes... it is kind of a dirty hack
|
|
||||||
fmt.Sprintf("SUDO_UID=%d", os.Getuid()),
|
|
||||||
"-v",
|
"-v",
|
||||||
fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()),
|
fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()),
|
||||||
"-v",
|
"-v",
|
||||||
@ -159,12 +139,6 @@ func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...s
|
|||||||
"/d2vm",
|
"/d2vm",
|
||||||
fmt.Sprintf("%s:%s", image, version),
|
fmt.Sprintf("%s:%s", image, version),
|
||||||
cmd,
|
cmd,
|
||||||
)
|
|
||||||
c := exec.CommandContext(ctx, "docker", append(a, args...)...)
|
|
||||||
if interactive {
|
|
||||||
c.Stdin = os.Stdin
|
|
||||||
}
|
}
|
||||||
c.Stdout = os.Stdout
|
return RunInteractiveAndRemove(ctx, append(a, args...)...)
|
||||||
c.Stderr = os.Stderr
|
|
||||||
return c.Run()
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
Run = RunNoOut
|
Run = RunNoOut
|
||||||
|
|
||||||
|
CommandContext = exec.CommandContext
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetDebug(debug bool) {
|
func SetDebug(debug bool) {
|
||||||
@ -37,11 +39,6 @@ func SetDebug(debug bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func RunDebug(ctx context.Context, c string, args ...string) error {
|
||||||
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
|
logrus.Debugf("$ %s %s", c, strings.Join(args, " "))
|
||||||
cmd := exec.CommandContext(ctx, c, args...)
|
cmd := exec.CommandContext(ctx, c, args...)
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
365
pkg/qemu/qemu.go
365
pkg/qemu/qemu.go
@ -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
|
|
||||||
}
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
100
syslinux.go
100
syslinux.go
@ -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{})
|
|
||||||
}
|
|
@ -2,19 +2,12 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk update --no-cache && \
|
||||||
|
apk add \
|
||||||
util-linux \
|
util-linux \
|
||||||
linux-virt \
|
linux-virt \
|
||||||
{{- if ge .Release.VersionID "3.17" }}
|
|
||||||
busybox-openrc \
|
|
||||||
busybox-mdev-openrc \
|
|
||||||
busybox-extras-openrc \
|
|
||||||
busybox-mdev-openrc \
|
|
||||||
{{- else }}
|
|
||||||
busybox-initscripts \
|
busybox-initscripts \
|
||||||
{{- end }}
|
openrc
|
||||||
openrc && \
|
|
||||||
find /boot -type l -exec rm {} \;
|
|
||||||
|
|
||||||
RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done
|
RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done
|
||||||
RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
|
RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
|
||||||
@ -29,22 +22,3 @@ allow-hotplug eth0\n\
|
|||||||
iface eth0 inet dhcp\n\
|
iface eth0 inet dhcp\n\
|
||||||
' > /etc/network/interfaces
|
' > /etc/network/interfaces
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .Luks }}
|
|
||||||
RUN apk add --no-cache cryptsetup && \
|
|
||||||
source /etc/mkinitfs/mkinitfs.conf && \
|
|
||||||
echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \
|
|
||||||
mkinitfs $(ls /lib/modules)
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
# we need to keep that at the end, because after it, we can't install packages without error anymore due to grub hooks
|
|
||||||
{{- if .Grub }}
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
{{- if .GrubBIOS }}
|
|
||||||
grub-bios \
|
|
||||||
{{- end }}
|
|
||||||
{{- if .GrubEFI }}
|
|
||||||
grub-efi \
|
|
||||||
{{- end }}
|
|
||||||
grub
|
|
||||||
{{- end }}
|
|
||||||
|
@ -2,47 +2,19 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
{{ $version := atoi .Release.Version }}
|
|
||||||
|
|
||||||
{{ if le $version 8 }}
|
|
||||||
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
||||||
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
|
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
RUN yum update -y
|
RUN yum update -y
|
||||||
|
|
||||||
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
|
RUN yum install -y kernel systemd NetworkManager e2fsprogs sudo && \
|
||||||
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 enable NetworkManager && \
|
||||||
systemctl unmask systemd-remount-fs.service && \
|
systemctl unmask systemd-remount-fs.service && \
|
||||||
systemctl unmask getty.target && \
|
systemctl unmask getty.target
|
||||||
find /boot -type l -exec rm {} \;
|
|
||||||
|
|
||||||
{{ if .Luks }}
|
RUN dracut --no-hostonly --regenerate-all --force && \
|
||||||
RUN yum install -y cryptsetup && \
|
cd /boot && \
|
||||||
dracut --no-hostonly --regenerate-all --force --install="/usr/sbin/cryptsetup"
|
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
|
||||||
{{ else }}
|
ln -s $(find . -name 'initramfs-*.img') initrd.img
|
||||||
RUN dracut --no-hostonly --regenerate-all --force
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
{{- if not .Grub }}
|
|
||||||
RUN cd /boot && \
|
|
||||||
mv $(find {{ if le $version 8 }}.{{ else }}/{{ end }} -name 'vmlinuz*') /boot/vmlinuz && \
|
|
||||||
mv $(find . -name 'initramfs-*.img') /boot/initrd.img
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
RUN yum clean all && \
|
|
||||||
rm -rf /var/cache/yum
|
|
||||||
|
@ -2,33 +2,13 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
{{- if eq .Release.VersionID "9" }}
|
RUN apt-get -y update && \
|
||||||
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 && \
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
linux-image-amd64 && \
|
linux-image-amd64
|
||||||
find /boot -type l -exec rm {} \;
|
|
||||||
|
|
||||||
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
|
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
||||||
systemd-sysv \
|
systemd-sysv \
|
||||||
systemd \
|
systemd \
|
||||||
{{- if .Grub }}
|
|
||||||
grub-common \
|
|
||||||
grub2-common \
|
|
||||||
{{- end }}
|
|
||||||
{{- if .GrubBIOS }}
|
|
||||||
grub-pc-bin \
|
|
||||||
{{- end }}
|
|
||||||
{{- if .GrubEFI }}
|
|
||||||
grub-efi-${ARCH}-bin \
|
|
||||||
{{- end }}
|
|
||||||
dbus \
|
dbus \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
isc-dhcp-client \
|
isc-dhcp-client \
|
||||||
@ -61,19 +41,3 @@ allow-hotplug eth0\n\
|
|||||||
iface eth0 inet dhcp\n\
|
iface eth0 inet dhcp\n\
|
||||||
' > /etc/network/interfaces
|
' > /etc/network/interfaces
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
{{- if .Luks }}
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
|
|
||||||
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook && \
|
|
||||||
update-initramfs -u -v
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
# needs to be after update-initramfs
|
|
||||||
{{- if not .Grub }}
|
|
||||||
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
|
|
||||||
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
RUN apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
@ -2,32 +2,18 @@ FROM {{ .Image }}
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
|
RUN apt-get update -y && \
|
||||||
apt-get update && \
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||||
linux-image-virtual \
|
linux-image-virtual \
|
||||||
initramfs-tools \
|
initramfs-tools \
|
||||||
systemd-sysv \
|
systemd-sysv \
|
||||||
systemd \
|
systemd \
|
||||||
{{- if .Grub }}
|
|
||||||
grub-common \
|
|
||||||
grub2-common \
|
|
||||||
{{- end }}
|
|
||||||
{{- if .GrubBIOS }}
|
|
||||||
grub-pc-bin \
|
|
||||||
{{- end }}
|
|
||||||
{{- if .GrubEFI }}
|
|
||||||
grub-efi-${ARCH}-bin \
|
|
||||||
{{- end }}
|
|
||||||
dbus \
|
dbus \
|
||||||
isc-dhcp-client \
|
isc-dhcp-client \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
iputils-ping && \
|
iputils-ping
|
||||||
find /boot -type l -exec rm {} \;
|
|
||||||
|
|
||||||
{{ if gt .Release.VersionID "16.04" }}
|
|
||||||
RUN systemctl preset-all
|
RUN systemctl preset-all
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
|
||||||
|
|
||||||
@ -54,17 +40,3 @@ allow-hotplug eth0\n\
|
|||||||
iface eth0 inet dhcp\n\
|
iface eth0 inet dhcp\n\
|
||||||
' > /etc/network/interfaces
|
' > /etc/network/interfaces
|
||||||
{{ end }}
|
{{ 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/*
|
|
||||||
|
15
version.go
15
version.go
@ -14,21 +14,8 @@
|
|||||||
|
|
||||||
package d2vm
|
package d2vm
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/qemu_img"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Arch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
|
||||||
Version = ""
|
Version = ""
|
||||||
BuildDate = ""
|
BuildDate = ""
|
||||||
Image = "linkacloud/d2vm"
|
Image = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
qemu_img.DockerImageName = Image
|
|
||||||
qemu_img.DockerImageVersion = Version
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user