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

33 Commits
v0.2.0 ... main

Author SHA1 Message Date
95633d344e docs: add CNAME to mkdocs build output
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>


skip-checks: true
2026-01-22 10:46:10 +01:00
6e54c731a4 feat: add AlmaLinux and Rocky Linux support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-22 09:52:01 +01:00
1e58d4dc42 chore: upgrade dependencies
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-22 09:24:00 +01:00
95537d5d2f centos: do not upgrade packages
close #71

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-22 09:22:43 +01:00
8a21a9cee8 feat: add --add-host flag to customize host-to-IP mapping in /etc/hosts
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 21:41:38 +01:00
0ecdf57a6a feat: add --dns and --dns-search flags to customize the vm dns configuration
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 21:41:38 +01:00
f799dc4891 feat: add --hostname flag to customize the vm hostname
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 21:41:37 +01:00
75a9f8d9a9 templates: improve docker build layers caching
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 21:41:37 +01:00
553f7f8d4b fix: squash image layers during build
close #75

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 19:05:18 +01:00
113541c1d6 chore: update distro releases
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2026-01-21 18:49:43 +01:00
John Rowley
897f49a452 chore(ci): call linter in ci 2025-04-12 11:28:20 +02:00
John Rowley
4d30dbc2b3 fix: ensure fs error is checked 2025-04-12 11:27:46 +02:00
dependabot[bot]
c15ce008db build(deps): bump github.com/docker/distribution
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.8.1+incompatible to 2.8.2+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 11:57:39 +02:00
fe4e9b2b3d [skip ci] docs: explain why root privileges are required
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2025-04-10 11:36:16 +02:00
b7e95d73b1 tests: fix manifest for centos:latest not found
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2025-04-10 11:25:26 +02:00
Lei Wang
85b144fe63 replace .Release.Version with .Release.VersionID to fix #65
Signed-off-by: Lei Wang <ssst0n3@gmail.com>
2025-04-10 11:12:58 +02:00
Will Scott
a0039dbae7 Skip disk conversion when format is raw
This skips the `convert2Img` step in favor of moving the file when targetting a raw format.
2025-03-26 02:14:33 +01:00
dc430fe1b7 chore: upgrade actions/cache to v3
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2025-03-25 13:09:34 +01:00
edgood1
dfd4d7dd43 when multiple kernels and initrds exist, use the latest 2025-03-02 12:44:55 +01:00
d8ee37833e add ubuntu 16.04 support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2024-08-23 15:08:21 +02:00
e31bc93074 centos-stream-9: fix Dockerfile template
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2024-07-09 19:05:28 +02:00
f711f8919d fix vhd format support
closes  #47

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2024-05-08 17:06:20 +02:00
Spencer von der Ohe
b27add5767 Fix typo in README.md
Signed-off-by: Spencer von der Ohe <s.vonderohe40@gmail.com>
2023-11-17 15:57:45 +01:00
1934915ae8 templates: clean package manager cache
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-14 12:42:52 +02:00
d54b3f9a2c arm64 support with grub-efi
* build / convert: add `--platform` flag to support linux/amd64 &
linux/arm64
* build: add `--pull` flag
* run/hetzner: add `--type` flag to select server type
* run/hetzner: add `--location` flag to select server location

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-13 17:42:39 +02:00
f8fc729486 grub and grub-efi not supported for CentOS
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-13 13:42:00 +02:00
a41bbdb745 add grub-efi support
* tests: increase timeout
* ci: split e2e tests

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-13 11:06:26 +02:00
d4c3476031 docs: update cli and README
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 19:23:48 +02:00
fb3ee62962 add fat32 boot partition support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 19:23:47 +02:00
384a4e436c docs: update cli and README
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:35 +02:00
a22bf9caf1 fix debian stretch support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:34 +02:00
4e533b8044 add grub support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:34 +02:00
a003e176f5 chore: bootloader abtraction
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 13:44:12 +02:00
44 changed files with 1697 additions and 695 deletions

View File

@@ -2,7 +2,6 @@ name: Tests and Build
on:
push:
branches: [ "*" ]
tags: [ "v*" ]
pull_request:
branches: [ main ]
@@ -21,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
@@ -33,7 +32,7 @@ jobs:
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
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
@@ -42,14 +41,32 @@ jobs:
restore-keys: |
${{ runner.os }}-tests-
- name: Run linter
run: make vet
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || make tests
e2e-tests:
name: End to end Tests
templates-tests:
name: Test Templates
runs-on: ubuntu-latest
strategy:
matrix:
image:
- ubuntu
- debian
- kalilinux
- alpine
- centos
- quay.io/centos/centos:stream
- almalinux
- rockylinux
steps:
- name: Free Disk Space (Ubuntu)
uses: linka-cloud/free-disk-space@main
if: matrix.image == 'quay.io/centos/centos:stream'
- name: Checkout
uses: actions/checkout@v3
with:
@@ -59,7 +76,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
@@ -71,7 +88,60 @@ jobs:
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
uses: actions/cache@v3
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.23
- ubuntu:22.04
- ubuntu:24.04
- debian:12
- debian:13
- centos:8
- quay.io/centos/centos:stream10
- almalinux:10
- rockylinux:9
steps:
- name: Free Disk Space (Ubuntu)
uses: linka-cloud/free-disk-space@main
- 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.25"
- 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@v3
with:
path: |
~/go/pkg/mod
@@ -81,7 +151,7 @@ jobs:
${{ runner.os }}-tests-
- name: Run end-to-end tests
run: make e2e
run: E2E_IMAGES=${{ matrix.image }} make e2e
docs-up-to-date:
name: Docs up to date
@@ -97,10 +167,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Share cache with other actions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
@@ -126,7 +196,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
@@ -142,7 +212,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
@@ -186,7 +256,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
@@ -202,7 +272,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
@@ -224,6 +294,7 @@ jobs:
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- templates-tests
- docs-up-to-date
- build
- e2e-tests
@@ -238,10 +309,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Share cache with other actions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
@@ -287,7 +358,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: "1.25"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
@@ -302,7 +373,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Share cache with other actions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod

4
.gitignore vendored
View File

@@ -11,8 +11,10 @@ dist/
images
/d2vm
/examples/build
/examples/full/demo-magic
/examples/full/inside
.goreleaser.yaml
docs/build
docs-src
/completions
/cmd/d2vm/run/sparsecat-linux-amd64
/cmd/d2vm/run/sparsecat-linux-*

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.20 as builder
FROM golang:1.25 as builder
WORKDIR /d2vm
@@ -35,9 +35,10 @@ RUN apt-get update && \
parted \
kpartx \
e2fsprogs \
dosfstools \
mount \
tar \
extlinux \
"$([ "$(uname -m)" = "x86_64" ] && echo extlinux)" \
cryptsetup-bin \
qemu-utils && \
apt-get clean && \

View File

@@ -64,10 +64,15 @@ docker-run:
.PHONY: tests
tests:
@go generate ./...
@go list .| xargs go test -exec sudo -count=1 -timeout 20m -v
@go list .| xargs go test -exec sudo -count=1 -timeout 60m -v -skip TestConfig
.PHONY: test-templates
test-templates:
@go generate ./...
@go test -exec sudo -count=1 -timeout 60m -v -run TestConfig/$(IMAGE)
e2e: docker-build .build
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e -args -images $(E2E_IMAGES)
docs-up-to-date:
@$(MAKE) cli-docs
@@ -87,7 +92,7 @@ install: docker-build
.build:
@go generate ./...
@go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
@CGO_ENABLED=0 go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
.PHONY: build-snapshot
build-snapshot: bin
@@ -116,7 +121,7 @@ completions: .build
.PHONY: examples
examples: build-dev
@mkdir -p examples/build
@for f in $$(find examples -type f -name '*Dockerfile' -maxdepth 1); do \
@for f in $$(find examples -maxdepth 1 -type f -name '*Dockerfile'); do \
echo "Building $$f"; \
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
done
@@ -132,7 +137,8 @@ serve-docs:
.PHONY: build-docs
build-docs: clean-docs cli-docs
@docker run --rm -v $(PWD):/docs linkacloud/mkdocs-material build -f /docs/docs/mkdocs.yml -d build
@docker run --rm -v $(PWD):/docs --user $$(id -u):$$(id -u) linkacloud/mkdocs-material build -f /docs/docs/mkdocs.yml -d build
@cp docs/CNAME docs/build/CNAME
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)

View File

@@ -34,6 +34,8 @@ Working and tested:
Luks support is available only on Debian buster+
- [x] Alpine
- [x] CentOS (8+)
- [x] Rocky Linux
- [x] AlmaLinux
Unsupported:
@@ -57,14 +59,23 @@ Obviously, **Distroless** images are not supported.
- udev
- parted
- e2fsprogs
- dosfstools (when using fat32)
- mount
- tar
- extlinux
- 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)
#### sudo or root privileges
*sudo* or root privileges are required for `d2vm` to performs operations that require root-level permissions, in particular:
- mounting disk images and loopback devices requires [elevated privileges](https://linux.die.net/man/2/mount)
- invoke `docker` commands, which require root-level permissions by default
## Getting started
### Install
@@ -80,7 +91,7 @@ alias d2vm="docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --p
```
```bash
wich d2vm
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
```
@@ -157,14 +168,21 @@ Usage:
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--add-host strings Add a custom host-to-IP mapping (host:ip) to the /etc/hosts file in the generated image
--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
--dns strings DNS servers to set in the generated image
--dns-search strings DNS search domains to set in the generated image
--force Override output qcow2 image
-h, --help help for convert
--hostname string Hostname to set in the generated image (default "localhost")
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
@@ -245,7 +263,7 @@ Welcome to Ubuntu 20.04.4 LTS!
Ubuntu 20.04.4 LTS localhost ttyS0
localhost login:
localhost login:
```
Log in using the *root* user and the password configured at build time.
@@ -312,16 +330,24 @@ Usage:
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--add-host strings Add a custom host-to-IP mapping (host:ip) to the /etc/hosts file in the generated image
--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
--dns strings DNS servers to set in the generated image
--dns-search strings DNS search domains to set in the generated image
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
-h, --help help for build
--hostname string Hostname to set in the generated image (default "localhost")
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")
@@ -346,7 +372,7 @@ 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
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.

43
bootloader.go Normal file
View File

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

View File

@@ -41,72 +41,10 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
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
)
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 ReleaseKali:
return syslinuxCfgDebian, nil
case ReleaseAlpine:
return syslinuxCfgAlpine, nil
case ReleaseCentOS:
return syslinuxCfgCentOS, nil
default:
return "", fmt.Errorf("%s: distribution not supported", osRelease.ID)
}
}
var formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vhd", "vhdx", "vmdk"}
type Builder interface {
Build(ctx context.Context) (err error)
@@ -114,7 +52,9 @@ type Builder interface {
}
type builder struct {
osRelease OSRelease
osRelease OSRelease
config Config
bootloader Bootloader
src string
img *image
@@ -127,8 +67,7 @@ type builder struct {
splitBoot bool
bootSize uint64
mbrPath string
bootFS BootFS
loDevice string
bootPart string
@@ -143,11 +82,23 @@ type builder struct {
luksPassword string
cmdLineExtra string
arch string
hostname string
dns []string
dnsSearch []string
hosts string
}
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string) (Builder, error) {
if err := checkDependencies(); err != nil {
return nil, err
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, hostname string, dns, dnsSearch []string, extraHosts map[string]string) (Builder, error) {
var arch string
switch platform {
case "linux/amd64":
arch = "x86_64"
case "linux/arm64", "linux/aarch64":
arch = "arm64"
default:
return nil, fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
}
if luksPassword != "" {
if !splitBoot {
@@ -167,6 +118,9 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
if !valid {
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
}
if f == "vhd" {
f = "vpc"
}
if splitBoot && bootSize < 50 {
return nil, fmt.Errorf("boot partition size must be at least 50MiB")
@@ -176,16 +130,41 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
return nil, fmt.Errorf("boot partition size must be less than the disk size")
}
mbrBin := ""
for _, v := range mbrPaths {
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
}
if bootLoader == "" {
bootLoader = "syslinux"
}
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
config, err := osRelease.Config()
if err != nil {
return nil, err
}
if splitBoot {
config.Kernel = strings.TrimPrefix(config.Kernel, "/boot")
config.Initrd = strings.TrimPrefix(config.Initrd, "/boot")
}
if bootFS == "" {
bootFS = BootFSExt4
}
if err := bootFS.Validate(); err != nil {
return nil, err
}
blp, err := BootloaderByName(bootLoader)
if err != nil {
return nil, err
}
bl, err := blp.New(config, osRelease, arch)
if err != nil {
return nil, err
}
if err := bl.Validate(bootFS); err != nil {
return nil, err
}
if size == 0 {
size = 10 * uint64(datasize.GB)
}
@@ -205,19 +184,39 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
// logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
// size = int64(s)
// }
if hostname == "" {
hostname = "localhost"
}
if len(dns) == 0 {
dns = []string{"8.8.8.8"}
}
hosts := hosts
for k, v := range extraHosts {
hosts += fmt.Sprintf("%s %s\n", v, k)
}
b := &builder{
osRelease: osRelease,
config: config,
bootloader: bl,
img: img,
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
diskOut: filepath.Join(workdir, disk+"."+format),
format: f,
size: size,
mbrPath: mbrBin,
mntPoint: filepath.Join(workdir, "/mnt"),
cmdLineExtra: cmdLineExtra,
splitBoot: splitBoot,
bootSize: bootSize,
bootFS: bootFS,
luksPassword: luksPassword,
arch: arch,
hostname: hostname,
dns: dns,
dnsSearch: dnsSearch,
hosts: hosts,
}
if err := b.checkDependencies(); err != nil {
return nil, err
}
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err
@@ -253,15 +252,12 @@ func (b *builder) Build(ctx context.Context) (err error) {
if err = b.setupRootFS(ctx); err != nil {
return err
}
if err = b.installKernel(ctx); err != nil {
if err = b.installBootloader(ctx); err != nil {
return err
}
if err = b.unmountImg(ctx); err != nil {
return err
}
if err = b.setupMBR(ctx); err != nil {
return err
}
if err = b.convert2Img(ctx); err != nil {
return err
}
@@ -359,7 +355,12 @@ func (b *builder) mountImg(ctx context.Context) error {
if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil {
return err
}
if err := exec.Run(ctx, "mkfs.ext4", b.bootPart); err != nil {
if b.bootFS.IsFat() {
err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart)
} else {
err = exec.Run(ctx, "mkfs.ext4", b.bootPart)
}
if err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil {
@@ -419,7 +420,7 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
return err
}
}
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot ext4 errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID)
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)
@@ -427,13 +428,13 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
if err := b.chWriteFile("/etc/resolv.conf", b.resolvConf(), 0644); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
if err := b.chWriteFile("/etc/hostname", b.hostname+"\n", perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
if err := b.chWriteFile("/etc/hosts", b.hosts, perm); err != nil {
return err
}
// TODO(adphi): is it the righ fix ?
@@ -443,13 +444,7 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
return err
}
// create a symlink to /boot for non-alpine images in order to have a consistent path
// even if the image is not split
if _, err := os.Stat(b.chPath("/boot/boot")); os.IsNotExist(err) {
if err := os.Symlink(".", b.chPath("/boot/boot")); err != nil {
return err
}
}
switch b.osRelease.ID {
case ReleaseAlpine:
by, err := os.ReadFile(b.chPath("/etc/inittab"))
@@ -464,75 +459,38 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
return err
}
return nil
case ReleaseUbuntu:
if b.osRelease.VersionID >= "20.04" {
return nil
}
fallthrough
case ReleaseDebian, ReleaseKali:
t, err := os.Readlink(b.chPath("/vmlinuz"))
if err != nil {
return err
}
if err := os.Symlink(t, b.chPath("/boot/vmlinuz")); err != nil {
return err
}
t, err = os.Readlink(b.chPath("/initrd.img"))
if err != nil {
return err
}
if err := os.Symlink(t, b.chPath("/boot/initrd.img")); err != nil {
return err
}
return nil
default:
return nil
}
}
func (b *builder) installKernel(ctx context.Context) error {
logrus.Infof("installing linux kernel")
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
return err
func (b *builder) cmdline(_ context.Context) string {
if !b.isLuksEnabled() {
return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra)
}
sysconfig, err := sysconfig(b.osRelease)
if err != nil {
return err
switch b.osRelease.ID {
case ReleaseAlpine:
return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra)
case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux:
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)
}
var cfg string
if b.isLuksEnabled() {
switch b.osRelease.ID {
case ReleaseAlpine:
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s root=/dev/mapper/root cryptdm=root", b.cmdLineExtra))
cfg = strings.Replace(cfg, "root=UUID="+b.rootUUID, "cryptroot=UUID="+b.cryptUUID, 1)
case ReleaseCentOS:
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s rd.luks.name=UUID=%s rd.luks.uuid=%s rd.luks.crypttab=0", b.cmdLineExtra, b.rootUUID, b.cryptUUID))
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
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s root=/dev/mapper/root cryptopts=target=root,source=UUID=%s,key=none,luks", b.cmdLineExtra, b.cryptUUID))
cfg = strings.Replace(cfg, "root=UUID="+b.rootUUID, "", 1)
}
} else {
cfg = fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra)
}
if err := b.chWriteFile("/boot/syslinux.cfg", cfg, perm); err != nil {
return err
}
return nil
}
func (b *builder) setupMBR(ctx context.Context) error {
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", b.mbrPath), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil {
return err
}
return nil
func (b *builder) installBootloader(ctx context.Context) error {
logrus.Infof("installing bootloader")
return b.bootloader.Setup(ctx, b.loDevice, b.mntPoint, b.cmdline(ctx))
}
func (b *builder) convert2Img(ctx context.Context) error {
logrus.Infof("converting to %s", b.format)
if b.format == "raw" {
return MoveFile(b.diskRaw, b.diskOut)
}
return exec.Run(ctx, "qemu-img", "convert", b.diskRaw, "-O", b.format, b.diskOut)
}
@@ -555,6 +513,21 @@ func (b *builder) isLuksEnabled() bool {
return b.luksPassword != ""
}
func (b *builder) resolvConf() string {
var sb strings.Builder
for _, v := range b.dns {
sb.WriteString("nameserver ")
sb.WriteString(v)
sb.WriteString("\n")
}
if len(b.dnsSearch) > 0 {
sb.WriteString("search ")
sb.WriteString(strings.Join(b.dnsSearch, " "))
sb.WriteString("\n")
}
return sb.String()
}
func (b *builder) Close() error {
return b.img.Close()
}
@@ -568,9 +541,13 @@ func block(path string, size uint64) error {
return f.Truncate(int64(size))
}
func checkDependencies() error {
func (b *builder) checkDependencies() error {
var merr error
for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "extlinux", "dd", "mkfs.ext4", "cryptsetup"} {
deps := []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "dd", "mkfs.ext4", "cryptsetup"}
if _, ok := b.bootloader.(*syslinux); ok {
deps = append(deps, "extlinux")
}
for _, v := range deps {
if _, err := exec2.LookPath(v); err != nil {
merr = multierr.Append(merr, err)
}

View File

@@ -1,160 +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/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", "", false)
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: "kalilinux/kali-rolling:latest",
kernel: "/vmlinuz",
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
},
{
image: "alpine:3.16",
kernel: "/boot/vmlinuz-virt",
initrd: "/boot/initramfs-virt",
sysconfig: syslinuxCfgAlpine,
},
{
image: "alpine",
kernel: "/boot/vmlinuz-virt",
initrd: "/boot/initramfs-virt",
sysconfig: syslinuxCfgAlpine,
},
{
image: "centos:8",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
},
{
image: "centos:latest",
kernel: "/boot/vmlinuz",
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
},
}
exec.SetDebug(true)
for _, test := range tests {
test := test
t.Run(test.image, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testSysconfig(t, ctx, test.image, test.sysconfig, test.kernel, test.initrd)
})
}
}

View File

@@ -79,9 +79,8 @@ var (
}
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...)
}
if luksPassword != "" && !splitBoot {
logrus.Warnf("luks password is set: enabling split boot")
splitBoot = true
if err := validateFlags(); err != nil {
return err
}
size, err := parseSize(size)
if err != nil {
@@ -90,16 +89,8 @@ var (
if file == "" {
file = filepath.Join(args[0], "Dockerfile")
}
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)
}
}
logrus.Infof("building docker image from %s", file)
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
if err := docker.Build(cmd.Context(), pull, tag, file, args[0], platform, buildArgs...); err != nil {
return err
}
if err := d2vm.Convert(
@@ -110,11 +101,19 @@ var (
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithBootLoader(bootloader),
d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize),
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
d2vm.WithLuksPassword(luksPassword),
d2vm.WithKeepCache(keepCache),
d2vm.WithPlatform(platform),
d2vm.WithPull(false),
d2vm.WithHostname(hostname),
d2vm.WithDNS(dns),
d2vm.WithDNSSearch(dnsSearch),
d2vm.WithExtraHosts(extraHosts),
); err != nil {
return err
}

View File

@@ -29,7 +29,7 @@ func maybeMakeContainerDisk(ctx context.Context) error {
return nil
}
logrus.Infof("creating container disk image %s", containerDiskTag)
if err := d2vm.MakeContainerDisk(ctx, output, containerDiskTag); err != nil {
if err := d2vm.MakeContainerDisk(ctx, output, containerDiskTag, platform); err != nil {
return err
}
if !push {

View File

@@ -15,11 +15,9 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/c2h5oh/datasize"
"github.com/sirupsen/logrus"
@@ -51,28 +49,14 @@ var (
}
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...)
}
if luksPassword != "" && !splitBoot {
logrus.Warnf("luks password is set: enabling split boot")
splitBoot = true
if err := validateFlags(); err != nil {
return err
}
img := args[0]
tag := "latest"
if parts := strings.Split(img, ":"); len(parts) > 1 {
img, tag = parts[0], parts[1]
}
img = fmt.Sprintf("%s:%s", img, tag)
size, err := parseSize(size)
if err != nil {
return err
}
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)
}
}
img := args[0]
found := false
if !pull {
imgs, err := docker.ImageList(cmd.Context(), img)
@@ -86,7 +70,7 @@ var (
}
if pull || !found {
logrus.Infof("pulling image %s", img)
if err := docker.Pull(cmd.Context(), img); err != nil {
if err := docker.Pull(cmd.Context(), platform, img); err != nil {
return err
}
}
@@ -98,11 +82,19 @@ var (
d2vm.WithOutput(output),
d2vm.WithCmdLineExtra(cmdLineExtra),
d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)),
d2vm.WithBootLoader(bootloader),
d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize),
d2vm.WithBootFS(d2vm.BootFS(bootFS)),
d2vm.WithLuksPassword(luksPassword),
d2vm.WithKeepCache(keepCache),
d2vm.WithPlatform(platform),
d2vm.WithPull(pull),
d2vm.WithHostname(hostname),
d2vm.WithDNS(dns),
d2vm.WithDNSSearch(dnsSearch),
d2vm.WithExtraHosts(extraHosts),
); err != nil {
return err
}
@@ -126,7 +118,6 @@ func parseSize(s string) (uint64, error) {
}
func init() {
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
convertCmd.Flags().AddFlagSet(buildFlags())
rootCmd.AddCommand(convertCmd)
}

View File

@@ -15,8 +15,11 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"go.linka.cloud/d2vm"
@@ -33,13 +36,78 @@ var (
containerDiskTag = ""
push bool
networkManager string
bootloader string
splitBoot bool
bootSize uint64
bootFS string
luksPassword string
keepCache bool
platform string
hostname string
dns []string
dnsSearch []string
hosts []string
extraHosts map[string]string
)
func validateFlags() (err 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)
}
}
extraHosts, err = validateHosts(hosts...)
if err != nil {
return fmt.Errorf("invalid --add-host value: %w", err)
}
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(), " "))
@@ -53,7 +121,28 @@ func buildFlags() *pflag.FlagSet {
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")
flags.StringVar(&hostname, "hostname", "localhost", "Hostname to set in the generated image")
flags.StringSliceVar(&dns, "dns", []string{}, "DNS servers to set in the generated image")
flags.StringSliceVar(&dnsSearch, "dns-search", []string{}, "DNS search domains to set in the generated image")
flags.StringSliceVar(&hosts, "add-host", []string{}, "Add a custom host-to-IP mapping (host:ip) to the /etc/hosts file in the generated image")
return flags
}
func validateHosts(vals ...string) (map[string]string, error) {
out := make(map[string]string)
for _, val := range vals {
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
k, v, ok := strings.Cut(val, ":")
if !ok || k == "" {
return nil, fmt.Errorf("bad format for add-host: %q", val)
}
out[k] = v
}
return out, nil
}

View File

@@ -70,6 +70,8 @@ func init() {
HetznerCmd.Flags().StringVarP(&hetznerSSHKeyPath, "ssh-key", "i", "", "d2vm image identity key")
HetznerCmd.Flags().BoolVar(&hetznerRemove, "rm", false, "remove server when done")
HetznerCmd.Flags().StringVarP(&hetznerServerName, "name", "n", "d2vm", "d2vm server name")
HetznerCmd.Flags().StringVarP(&hetznerVMType, "type", "t", hetznerVMType, "d2vm server type")
HetznerCmd.Flags().StringVarP(&hetznerDatacenter, "location", "l", hetznerDatacenter, "d2vm server location")
}
func Hetzner(cmd *cobra.Command, args []string) {
@@ -113,10 +115,23 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
if err != nil {
return err
}
img, _, err := c.Image.GetByName(ctx, serverImg)
arch := "amd64"
harch := hcloud.ArchitectureX86
if strings.HasPrefix(strings.ToLower(hetznerVMType), "cax") {
harch = hcloud.ArchitectureARM
arch = "arm64"
}
sparsecatBin, err := Sparsecat(arch)
if err != nil {
return err
}
imgs, _, err := c.Image.List(ctx, hcloud.ImageListOpts{Name: serverImg, Architecture: []hcloud.Architecture{harch}})
if err != nil {
return err
}
if len(imgs) == 0 {
return fmt.Errorf("no image found with name %s", serverImg)
}
l, _, err := c.Location.Get(ctx, hetznerDatacenter)
if err != nil {
return err
@@ -125,9 +140,9 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
sres, _, err := c.Server.Create(ctx, hcloud.ServerCreateOpts{
Name: hetznerServerName,
ServerType: st,
Image: img,
Image: imgs[0],
Location: l,
StartAfterCreate: hcloud.Bool(false),
StartAfterCreate: hcloud.Ptr(false),
})
if err != nil {
return err
@@ -186,7 +201,7 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.
return err
}
defer f.Close()
if _, err := io.Copy(f, bytes.NewReader(sparsecatBinary)); err != nil {
if _, err := io.Copy(f, bytes.NewReader(sparsecatBin)); err != nil {
return err
}
if err := f.Close(); err != nil {

View File

@@ -28,6 +28,7 @@ var (
arch string
cpus uint
mem uint
bios string
qemuCmd string
qemuDetached bool
networking string
@@ -71,6 +72,8 @@ func init() {
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
flags.StringVar(&bios, "bios", "", "Path to the optional bios binary")
// Backend configuration
flags.StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
flags.BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background")
@@ -105,6 +108,7 @@ func Qemu(cmd *cobra.Command, args []string) {
qemu.WithStdin(os.Stdin),
qemu.WithStdout(os.Stdout),
qemu.WithStderr(os.Stderr),
qemu.WithBios(bios),
}
if enableGUI {
opts = append(opts, qemu.WithGUI())

View File

@@ -1,4 +1,5 @@
//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.
//
@@ -33,7 +34,21 @@ import (
)
//go:embed sparsecat-linux-amd64
var sparsecatBinary []byte
var sparsecatAmdBinary []byte
//go:embed sparsecat-linux-arm64
var sparsecatArmBinary []byte
func Sparsecat(arch string) ([]byte, error) {
switch arch {
case "amd64":
return sparsecatAmdBinary, nil
case "arm64":
return sparsecatArmBinary, nil
default:
return nil, fmt.Errorf("unsupported architecture: %s", arch)
}
}
// Handle flags with multiple occurrences
type MultipleFlag []string

89
config.go Normal file
View File

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

187
config_test.go Normal file
View File

@@ -0,0 +1,187 @@
// 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.uber.org/multierr"
"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) func() error {
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)
var fns []func() error
clean := func() (err error) {
for _, v := range fns {
err = multierr.Append(err, v())
}
return err
}
fns = append(fns, func() error {
return 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))
fns = append(fns, func() error {
return docker.Remove(ctx, imgUUID)
})
// we don't need to test the kernel location if grub is enabled
if grubBIOS || grubEFI {
return clean
}
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel))
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd))
return clean
}
func TestConfig(t *testing.T) {
t.Parallel()
tests := []struct {
image string
config Config
}{
{
image: "ubuntu:20.04",
config: configUbuntu,
},
{
image: "ubuntu:22.04",
config: configUbuntu,
},
{
image: "ubuntu:24.04",
config: configUbuntu,
},
{
image: "ubuntu:latest",
config: configUbuntu,
},
{
image: "debian:11",
config: configDebian,
},
{
image: "debian:12",
config: configDebian,
},
{
image: "debian:13",
config: configDebian,
},
{
image: "debian:latest",
config: configDebian,
},
{
image: "kalilinux/kali-rolling:latest",
config: configDebian,
},
{
image: "alpine:3.20",
config: configAlpine,
},
{
image: "alpine",
config: configAlpine,
},
{
image: "centos:8",
config: configCentOS,
},
{
image: "quay.io/centos/centos:stream9",
config: configCentOS,
},
{
image: "quay.io/centos/centos:stream10",
config: configCentOS,
},
{
image: "almalinux:10",
config: configCentOS,
},
{
image: "rockylinux:9",
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, "-")
var clean func() error
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clean = testConfig(t, ctx, name, test.image, test.config, luks, grubBIOS, grubEFI)
})
defer func() {
if clean != nil {
_ = clean()
}
}()
}
}
}
})
}
}

View File

@@ -36,7 +36,7 @@ ADD --chown=%[1]d:%[1]d %[2]s /disk/
`
)
func MakeContainerDisk(ctx context.Context, path string, tag string) error {
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
@@ -60,7 +60,7 @@ func MakeContainerDisk(ctx context.Context, path string, tag string) error {
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, tag, dockerfile, tmpPath); err != nil {
if err := docker.Build(ctx, false, tag, dockerfile, tmpPath, platform); err != nil {
return fmt.Errorf("failed to build container disk: %w", err)
}
return nil

View File

@@ -41,7 +41,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
r, err := FetchDockerImageOSRelease(ctx, img)
if err != nil {
return err
}
@@ -51,7 +51,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
}
if !o.raw {
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "")
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.hasGrubBIOS(), o.hasGrubEFI())
if err != nil {
return err
}
@@ -67,7 +67,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
return err
}
logrus.Infof("building kernel enabled image")
if err := docker.Build(ctx, imgUUID, p, dir); err != nil {
if err := docker.Build(ctx, o.pull, imgUUID, p, dir, o.platform); err != nil {
return err
}
if !o.keepCache {
@@ -88,7 +88,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
if format == "" {
format = "raw"
}
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize, o.luksPassword)
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.luksPassword, o.bootLoader, o.platform, o.hostname, o.dns, o.dnsSearch, o.hosts)
if err != nil {
return err
}

View File

@@ -22,14 +22,31 @@ type convertOptions struct {
output string
cmdLineExtra string
networkManager NetworkManager
bootLoader string
raw bool
splitBoot bool
bootSize uint64
bootFS BootFS
luksPassword string
keepCache bool
platform string
pull bool
hostname string
dns []string
dnsSearch []string
hosts map[string]string
}
func (o *convertOptions) hasGrubBIOS() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-bios"
}
func (o *convertOptions) hasGrubEFI() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-efi"
}
func WithSize(size uint64) ConvertOption {
@@ -62,6 +79,12 @@ func WithNetworkManager(networkManager NetworkManager) ConvertOption {
}
}
func WithBootLoader(bootLoader string) ConvertOption {
return func(o *convertOptions) {
o.bootLoader = bootLoader
}
}
func WithRaw(raw bool) ConvertOption {
return func(o *convertOptions) {
o.raw = raw
@@ -80,6 +103,12 @@ func WithBootSize(bootSize uint64) ConvertOption {
}
}
func WithBootFS(bootFS BootFS) ConvertOption {
return func(o *convertOptions) {
o.bootFS = bootFS
}
}
func WithLuksPassword(password string) ConvertOption {
return func(o *convertOptions) {
o.luksPassword = password
@@ -91,3 +120,39 @@ func WithKeepCache(b bool) ConvertOption {
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
}
}
func WithHostname(hostname string) ConvertOption {
return func(o *convertOptions) {
o.hostname = hostname
}
}
func WithDNS(dns []string) ConvertOption {
return func(o *convertOptions) {
o.dns = dns
}
}
func WithDNSSearch(dnsSearch []string) ConvertOption {
return func(o *convertOptions) {
o.dnsSearch = dnsSearch
}
}
func WithExtraHosts(hosts map[string]string) ConvertOption {
return func(o *convertOptions) {
o.hosts = hosts
}
}

View File

@@ -140,7 +140,7 @@ RUN rm -rf /etc/apk
require.NoError(t, os.WriteFile(filepath.Join(tmp, "hostname"), []byte("d2vm-flatten-test"), perm))
require.NoError(t, os.WriteFile(filepath.Join(tmp, "resolv.conf"), []byte("nameserver 8.8.8.8"), perm))
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
require.NoError(t, docker.Build(ctx, img, "", tmp))
require.NoError(t, docker.Build(ctx, false, img, "", tmp, "linux/amd64"))
defer docker.Remove(ctx, img)
imgTmp := filepath.Join(tmp, "image")

View File

@@ -18,6 +18,7 @@ import (
_ "embed"
"fmt"
"io"
"strconv"
"text/template"
"github.com/sirupsen/logrus"
@@ -36,10 +37,10 @@ var alpineDockerfile string
var centOSDockerfile string
var (
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Parse(ubuntuDockerfile))
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Parse(debianDockerfile))
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Parse(alpineDockerfile))
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Parse(centOSDockerfile))
ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile))
debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile))
alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile))
centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile))
)
type NetworkManager string
@@ -65,15 +66,21 @@ type Dockerfile struct {
Release OSRelease
NetworkManager NetworkManager
Luks bool
GrubBIOS bool
GrubEFI bool
tmpl *template.Template
}
func (d Dockerfile) Grub() bool {
return d.GrubBIOS || d.GrubEFI
}
func (d Dockerfile) Render(w io.Writer) error {
return d.tmpl.Execute(w, d)
}
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks}
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grubBIOS, grubEFI bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, GrubBIOS: grubBIOS, GrubEFI: grubEFI}
var net NetworkManager
switch release.ID {
case ReleaseDebian:
@@ -84,14 +91,18 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo
net = NetworkManagerIfupdown2
case ReleaseUbuntu:
d.tmpl = ubuntuDockerfileTemplate
net = NetworkManagerNetplan
if release.VersionID < "18.04" {
net = NetworkManagerIfupdown2
} else {
net = NetworkManagerNetplan
}
case ReleaseAlpine:
d.tmpl = alpineDockerfileTemplate
net = NetworkManagerIfupdown2
if networkManager == NetworkManagerNetplan {
return d, fmt.Errorf("netplan is not supported on alpine")
}
case ReleaseCentOS:
case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux:
d.tmpl = centOSDockerfileTemplate
net = NetworkManagerNone
if networkManager != "" && networkManager != NetworkManagerNone {
@@ -101,7 +112,7 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
}
if d.NetworkManager == "" {
if release.ID != ReleaseCentOS {
if release.ID != ReleaseCentOS && release.ID != ReleaseRocky && release.ID != ReleaseAlmaLinux {
logrus.Warnf("no network manager specified, using distribution defaults: %s", net)
}
d.NetworkManager = net
@@ -111,3 +122,7 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo
}
return d, nil
}
var tplFuncs = template.FuncMap{
"atoi": strconv.Atoi,
}

View File

@@ -9,17 +9,25 @@ d2vm build [context directory] [flags]
### Options
```
--add-host strings Add a custom host-to-IP mapping (host:ip) to the /etc/hosts file in the generated image
--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
--dns strings DNS servers to set in the generated image
--dns-search strings DNS search domains to set in the generated image
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
-h, --help help for build
--hostname string Hostname to set in the generated image (default "localhost")
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more
-s, --size string The output image size (default "10G")

View File

@@ -9,15 +9,22 @@ d2vm convert [docker image] [flags]
### Options
```
--add-host strings Add a custom host-to-IP mapping (host:ip) to the /etc/hosts file in the generated image
--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
--dns strings DNS servers to set in the generated image
--dns-search strings DNS search domains to set in the generated image
--force Override output qcow2 image
-h, --help help for convert
--hostname string Hostname to set in the generated image (default "localhost")
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vhd vhdx vmdk (default "disk0.qcow2")
-p, --password string Optional root user password
--platform string Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported (default "linux/amd64")
--pull Always pull docker image
--push Push the container disk image to the registry
--raw Just convert the container to virtual machine image without installing anything more

View File

@@ -9,12 +9,14 @@ d2vm run hetzner [options] image-path [flags]
### Options
```
-h, --help help for hetzner
-n, --name string d2vm server name (default "d2vm")
--rm remove server when done
-i, --ssh-key string d2vm image identity key
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
-u, --user string d2vm image ssh user (default "root")
-h, --help help for hetzner
-l, --location string d2vm server location (default "hel1-dc2")
-n, --name string d2vm server name (default "d2vm")
--rm remove server when done
-i, --ssh-key string d2vm image identity key
--token string Hetzner Cloud API token [$HETZNER_TOKEN]
-t, --type string d2vm server type (default "cx11")
-u, --user string d2vm image ssh user (default "root")
```
### Options inherited from parent commands

View File

@@ -9,8 +9,9 @@ d2vm run qemu [options] [image-path] [flags]
### Options
```
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "hvf:tcg")
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "kvm:tcg")
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
--bios string Path to the optional bios binary
--cpus uint Number of CPUs (default 1)
--detached Set qemu container to run in the background
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])

View File

@@ -18,6 +18,7 @@ import (
"bufio"
"bytes"
"context"
"flag"
"io"
"os"
"path/filepath"
@@ -36,6 +37,7 @@ import (
type test struct {
name string
args []string
efi bool
}
type img struct {
@@ -43,14 +45,27 @@ type img struct {
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"},
}
var (
images = []img{
{name: "alpine:3.23", luks: "Enter passphrase for /dev/sda2:"},
{name: "ubuntu:22.04", luks: "Please unlock disk root:"},
{name: "ubuntu:24.04", luks: "Please unlock disk root:"},
{name: "debian:12", luks: "Please unlock disk root:"},
{name: "debian:13", luks: "Please unlock disk root:"},
{name: "centos:8", luks: "Please enter passphrase for disk"},
{name: "quay.io/centos/centos:stream10", luks: "Please enter passphrase for disk"},
{name: "almalinux:10", luks: "Please enter passphrase for disk"},
{name: "rockylinux:9", 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)
@@ -62,10 +77,39 @@ func TestConvert(t *testing.T) {
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 {
@@ -75,14 +119,17 @@ func TestConvert(t *testing.T) {
require.NoError(os.MkdirAll(dir, os.ModePerm))
defer os.RemoveAll(dir)
for _, img := range images {
for _, img := range testImgs {
if (strings.Contains(img.name, "centos") || strings.Contains(img.name, "almalinux") || strings.Contains(img.name, "rocky")) && 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")
out := filepath.Join(dir, strings.NewReplacer(":", "-", ".", "-", "/", "-").Replace(img.name)+".qcow2")
if _, err := os.Stat(out); err == nil {
require.NoError(os.Remove(out))
@@ -102,6 +149,8 @@ func TestConvert(t *testing.T) {
login := []byte("login:")
password := []byte("Password:")
s := bufio.NewScanner(outr)
// fix failed to scan output: bufio.Scanner: token too long
s.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)
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
@@ -153,7 +202,11 @@ func TestConvert(t *testing.T) {
cancel()
}
}()
if err := qemu.Run(ctx, out, qemu.WithStdin(inr), qemu.WithStdout(io.MultiWriter(outw, os.Stdout)), qemu.WithStderr(io.Discard), qemu.WithMemory(2048)); err != nil && !success.Load() {
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 Normal file
View File

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

91
go.mod
View File

@@ -1,69 +1,60 @@
module go.linka.cloud/d2vm
go 1.20
go 1.25
require (
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0
github.com/google/go-containerregistry v0.14.0
github.com/google/uuid v1.3.0
github.com/hetznercloud/hcloud-go v1.35.2
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.18.0
github.com/google/go-containerregistry v0.20.7
github.com/google/uuid v1.6.0
github.com/hetznercloud/hcloud-go v1.59.2
github.com/joho/godotenv v1.5.1
github.com/pkg/sftp v1.10.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/svenwiltink/sparsecat v1.0.0
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
github.com/pkg/sftp v1.13.10
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/svenwiltink/sparsecat v1.0.1
go.linka.cloud/console v0.0.0-20221115093718-f725374f9010
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/sys v0.7.0
golang.org/x/crypto v0.47.0
golang.org/x/sys v0.40.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/creack/pty v1.1.15 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.4+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.4+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/docker/cli v29.1.5+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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.5.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.29.0 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.11.1
golang.org/x/crypto => golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b
google.golang.org/protobuf => google.golang.org/protobuf v1.29.1
)

256
go.sum
View File

@@ -1,200 +1,146 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v23.0.4+incompatible h1:xClB7PsiATttDHj8ce5qvJcikiApNy7teRR1XkoBZGs=
github.com/docker/cli v23.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw=
github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hetznercloud/hcloud-go v1.35.2 h1:eEDtmDiI2plZ2UQmj4YpiYse5XbtpXOUBpAdIOLxzgE=
github.com/hetznercloud/hcloud-go v1.35.2/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hetznercloud/hcloud-go v1.59.2 h1:NkCPwYiPv85FnOV3IW9/gxfW61TPIUSwyPHRSLwCkHA=
github.com/hetznercloud/hcloud-go v1.59.2/go.mod h1:oTebZCjd+osj75jlI76Z+zjN1sTxmMiQ1MWoO8aRl1c=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/svenwiltink/sparsecat v1.0.0 h1:SBDEIImxhD//8MskqodFR9VcixWKkZAPAW35nmA4vcw=
github.com/svenwiltink/sparsecat v1.0.0/go.mod h1:TdtvJbeTZcd+3cMQpttW6MJl/iPGZT0GHmckep0hoxU=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b h1:LccDAOuhRuyJqirU7I34UEEoYvuWWJ2jbs2Hyuc2aYU=
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b/go.mod h1:P/4MMfexPBBlmMpefLne7phPoNice1vhX65ZqOfzGII=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/svenwiltink/sparsecat v1.0.1 h1:GZLaZ4ZunREtRMOzxb1KOnLu9qzKTkilvYrifWlyx1M=
github.com/svenwiltink/sparsecat v1.0.1/go.mod h1:TdtvJbeTZcd+3cMQpttW6MJl/iPGZT0GHmckep0hoxU=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
go.linka.cloud/console v0.0.0-20221115093718-f725374f9010 h1:H7QTK+cHKn6Qh8SjBUheQjAFtqjsZ8vwnHEqNYTzLCk=
go.linka.cloud/console v0.0.0-20221115093718-f725374f9010/go.mod h1:w1z+5gBd/zKt0xt33JTP3GdeT9c0/vuaQeKPf8PgeB0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

76
grub.go Normal file
View File

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

65
grub_bios.go Normal file
View File

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

102
grub_common.go Normal file
View File

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

71
grub_efi.go Normal file
View File

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

View File

@@ -16,12 +16,8 @@ package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
@@ -30,12 +26,14 @@ import (
)
const (
ReleaseUbuntu Release = "ubuntu"
ReleaseDebian Release = "debian"
ReleaseAlpine Release = "alpine"
ReleaseCentOS Release = "centos"
ReleaseRHEL Release = "rhel"
ReleaseKali Release = "kali"
ReleaseUbuntu Release = "ubuntu"
ReleaseDebian Release = "debian"
ReleaseAlpine Release = "alpine"
ReleaseCentOS Release = "centos"
ReleaseRHEL Release = "rhel"
ReleaseKali Release = "kali"
ReleaseRocky Release = "rocky"
ReleaseAlmaLinux Release = "almalinux"
)
type Release string
@@ -52,6 +50,10 @@ func (r Release) Supported() bool {
return true
case ReleaseCentOS:
return true
case ReleaseRocky:
return true
case ReleaseAlmaLinux:
return true
case ReleaseRHEL:
return false
default:
@@ -83,6 +85,10 @@ func (r OSRelease) SupportsLUKS() bool {
return true
case ReleaseCentOS:
return true
case ReleaseRocky:
return true
case ReleaseAlmaLinux:
return true
case ReleaseAlpine:
return true
case ReleaseRHEL:
@@ -107,40 +113,8 @@ func ParseOSRelease(s string) (OSRelease, error) {
return o, nil
}
const (
osReleaseDockerfile = `
FROM {{ . }}
ENTRYPOINT [""]
CMD ["/bin/cat", "/etc/os-release"]
`
)
var (
osReleaseDockerfileTemplate = template.Must(template.New("osrelease.Dockerfile").Parse(osReleaseDockerfile))
)
func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) {
d := filepath.Join(tmpPath, "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)
func FetchDockerImageOSRelease(ctx context.Context, img string) (OSRelease, error) {
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", "--entrypoint", "cat", img, "/etc/os-release")
if err != nil {
return OSRelease{}, err
}

View File

@@ -50,11 +50,14 @@ func CmdOut(ctx context.Context, args ...string) (string, string, error) {
return exec.RunOut(ctx, "docker", args...)
}
func Build(ctx context.Context, tag, dockerfile, dir string, buildArgs ...string) error {
func Build(ctx context.Context, pull bool, tag, dockerfile, dir, platform string, buildArgs ...string) error {
if dockerfile == "" {
dockerfile = filepath.Join(dir, "Dockerfile")
}
args := []string{"image", "build", "-t", tag, "-f", dockerfile}
args := []string{"image", "build", "-t", tag, "-f", dockerfile, "--platform", platform}
if pull {
args = append(args, "--pull")
}
for _, v := range buildArgs {
args = append(args, "--build-arg", v)
}
@@ -96,8 +99,8 @@ func ImageSave(ctx context.Context, tag, file string) error {
return Cmd(ctx, "image", "save", "-o", file, tag)
}
func Pull(ctx context.Context, tag string) error {
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 {

View File

@@ -44,6 +44,7 @@ type config struct {
arch string
cpus uint
memory uint
bios string
accel string
detached bool
qemuBinPath string
@@ -92,6 +93,12 @@ func WithMemory(memory uint) Option {
}
}
func WithBios(bios string) Option {
return func(c *config) {
c.bios = bios
}
}
func WithAccel(accel string) Option {
return func(c *config) {
c.accel = accel

View File

@@ -197,14 +197,16 @@ func (c *config) buildQemuCmdline() ([]string, error) {
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" {
if runtime.GOARCH == "arm64" {
qemuArgs = append(qemuArgs, "-cpu", "host")
} else {
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
}
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

100
syslinux.go Normal file
View File

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

View File

@@ -1,9 +1,8 @@
FROM {{ .Image }}
FROM {{ .Image }} AS rootfs
USER root
RUN apk update --no-cache && \
apk add \
RUN apk add --no-cache \
util-linux \
linux-virt \
{{- if ge .Release.VersionID "3.17" }}
@@ -14,7 +13,8 @@ RUN apk update --no-cache && \
{{- else }}
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 devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done
@@ -30,9 +30,25 @@ iface eth0 inet dhcp\n\
' > /etc/network/interfaces
{{ end }}
{{- if .Luks }}
{{ 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 || true
{{- end }}
FROM scratch
COPY --from=rootfs / /

View File

@@ -1,12 +1,13 @@
FROM {{ .Image }}
FROM {{ .Image }} AS rootfs
USER root
{{ if and (eq .Release.ID "centos") (le (atoi .Release.VersionID) 8) }}
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
{{ end }}
RUN yum update -y
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
RUN yum install -y \
kernel \
systemd \
@@ -16,9 +17,15 @@ RUN yum install -y \
systemctl enable NetworkManager && \
systemctl unmask systemd-remount-fs.service && \
systemctl unmask getty.target && \
cd /boot && \
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
ln -s $(find . -name 'initramfs-*.img') initrd.img
mkdir -p /boot && \
find /boot -type l -exec rm {} \;
{{- if .GrubBIOS }}
RUN yum install -y grub2
{{- end }}
{{- if .GrubEFI }}
RUN yum install -y grub2 grub2-efi-x64 grub2-efi-x64-modules
{{- end }}
{{ if .Luks }}
RUN yum install -y cryptsetup && \
@@ -28,3 +35,16 @@ RUN dracut --no-hostonly --regenerate-all --force
{{ end }}
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{- if not .Grub }}
RUN cd /boot && \
mv $(find / -name 'vmlinuz*') /boot/vmlinuz && \
mv $(find . -name 'initramfs-*.img' -o -name initrd) /boot/initrd.img
{{- end }}
RUN yum clean all && \
rm -rf /var/cache/yum
FROM scratch
COPY --from=rootfs / /

View File

@@ -1,12 +1,22 @@
FROM {{ .Image }}
FROM {{ .Image }} AS rootfs
USER root
RUN apt-get -y update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-amd64
{{- if eq .Release.VersionID "9" }}
RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list && \
echo "deb-src http://archive.debian.org/debian stretch main" >> /etc/apt/sources.list && \
echo "deb http://archive.debian.org/debian stretch-backports main" >> /etc/apt/sources.list && \
echo "deb http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list && \
echo "deb-src http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list
{{- end }}
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-amd64 && \
find /boot -type l -exec rm {} \;
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
systemd-sysv \
systemd \
dbus \
@@ -14,6 +24,17 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
isc-dhcp-client \
iputils-ping
{{- if .Grub }}
RUN DEBIAN_FRONTEND=noninteractive apt install -y grub-common grub2-common
{{- end }}
{{- if .GrubBIOS }}
RUN DEBIAN_FRONTEND=noninteractive apt install -y grub-pc-bin
{{- end }}
{{- if .GrubEFI }}
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
DEBIAN_FRONTEND=noninteractive apt install -y grub-efi-${ARCH}-bin
{{- end }}
RUN systemctl preset-all
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
@@ -48,3 +69,16 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cr
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook && \
update-initramfs -u -v
{{- end }}
# needs to be after update-initramfs
{{- if not .Grub }}
RUN mv $(ls -t /boot/vmlinuz-* | head -n 1) /boot/vmlinuz && \
mv $(ls -t /boot/initrd.img-* | head -n 1) /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM scratch
COPY --from=rootfs / /

View File

@@ -1,19 +1,33 @@
FROM {{ .Image }}
FROM {{ .Image }} AS rootfs
USER root
RUN apt-get update -y && \
RUN ARCH="$([ "$(uname -m)" = "x86_64" ] && echo amd64 || echo arm64)"; \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-virtual \
initramfs-tools \
systemd-sysv \
systemd \
{{- if .Grub }}
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-${ARCH}-bin \
{{- end }}
dbus \
isc-dhcp-client \
iproute2 \
iputils-ping
iputils-ping && \
find /boot -type l -exec rm {} \;
{{ if gt .Release.VersionID "16.04" }}
RUN systemctl preset-all
{{ end }}
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
@@ -45,3 +59,16 @@ iface eth0 inet dhcp\n\
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 $(ls -t /boot/vmlinuz-* | head -n 1) /boot/vmlinuz && \
mv $(ls -t /boot/initrd.img-* | head -n 1) /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM scratch
COPY --from=rootfs / /

View File

@@ -15,10 +15,14 @@
package d2vm
import (
"fmt"
"runtime"
"go.linka.cloud/d2vm/pkg/qemu_img"
)
var (
Arch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
Version = ""
BuildDate = ""
Image = "linkacloud/d2vm"