Compare commits

...

12 Commits
v0.2.0 ... main

Author SHA1 Message Date
Adphi 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
Adphi 1934915ae8
templates: clean package manager cache
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-14 12:42:52 +02:00
Adphi 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
Adphi 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
Adphi 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
Adphi d4c3476031
docs: update cli and README
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 19:23:48 +02:00
Adphi fb3ee62962
add fat32 boot partition support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 19:23:47 +02:00
Adphi 384a4e436c
docs: update cli and README
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:35 +02:00
Adphi a22bf9caf1
fix debian stretch support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:34 +02:00
Adphi 4e533b8044
add grub support
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
2023-09-11 18:13:34 +02:00
Adphi 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 1328 additions and 475 deletions

View File

@ -45,9 +45,17 @@ jobs:
- 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
steps:
- name: Checkout
@ -70,6 +78,53 @@ jobs:
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || IMAGE=${{ matrix.image }} make test-templates
e2e-tests:
name: End to end Tests
runs-on: ubuntu-latest
strategy:
matrix:
image:
- alpine:3.17
- ubuntu:20.04
- ubuntu:22.04
- debian:10
- debian:11
- centos:8
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system ovmf
- name: Share cache with other actions
uses: actions/cache@v2
with:
@ -81,7 +136,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
@ -224,6 +279,7 @@ jobs:
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- templates-tests
- docs-up-to-date
- build
- e2e-tests

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

@ -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

View File

@ -57,9 +57,10 @@ 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)
@ -80,7 +81,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,7 +158,9 @@ Usage:
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build
@ -165,6 +168,7 @@ Flags:
--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
@ -312,7 +316,9 @@ Usage:
Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
@ -322,6 +328,8 @@ Flags:
--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")

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,18 @@ type builder struct {
luksPassword string
cmdLineExtra string
arch 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 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 +113,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 +125,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)
}
@ -207,17 +181,23 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
// }
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,
}
if err := b.checkDependencies(); err != nil {
return nil, err
}
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err
@ -253,15 +233,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 +336,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 +401,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)
@ -443,13 +425,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,71 +440,31 @@ 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:
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 {
@ -568,9 +504,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,15 @@ 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),
); 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,15 @@ 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),
); err != nil {
return err
}
@ -126,7 +114,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,67 @@ var (
containerDiskTag = ""
push bool
networkManager string
bootloader string
splitBoot bool
bootSize uint64
bootFS string
luksPassword string
keepCache bool
platform string
)
func validateFlags() error {
switch platform {
case "linux/amd64":
if bootloader == "" {
bootloader = "syslinux"
}
case "linux/arm64", "linux/aarch64":
platform = "linux/arm64"
if bootloader == "" {
bootloader = "grub-efi"
}
if bootloader != "grub-efi" {
return fmt.Errorf("unsupported bootloader for platform %s: %s, only grub-efi is supported", platform, bootloader)
}
default:
return fmt.Errorf("unexpected platform: %s, supported platforms: linux/amd64, linux/arm64", platform)
}
if luksPassword != "" && !splitBoot {
logrus.Warnf("luks password is set: enabling split boot")
splitBoot = true
}
if bootFS := d2vm.BootFS(bootFS); bootFS != "" && !bootFS.IsSupported() {
return fmt.Errorf("invalid boot filesystem: %s", bootFS)
}
if bootFS != "" && !splitBoot {
logrus.Warnf("boot filesystem is set: enabling split boot")
splitBoot = true
}
efi := bootloader == "grub-efi" || bootloader == "grub"
if efi && !splitBoot {
logrus.Warnf("grub-efi bootloader is set: enabling split boot")
splitBoot = true
}
if efi && bootFS != "" && bootFS != "fat32" {
return fmt.Errorf("grub-efi bootloader only supports fat32 boot filesystem")
}
if efi && bootFS == "" {
logrus.Warnf("grub-efi bootloader is set: enabling fat32 boot filesystem")
bootFS = "fat32"
}
if push && tag == "" {
return fmt.Errorf("tag is required when pushing container disk image")
}
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
if !force {
return fmt.Errorf("%s already exists", output)
}
}
return nil
}
func buildFlags() *pflag.FlagSet {
flags := pflag.NewFlagSet("build", pflag.ExitOnError)
flags.StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
@ -53,7 +110,11 @@ 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")
return flags
}

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:
return configCentOS, nil
default:
return Config{}, fmt.Errorf("%s: distribution not supported", r.ID)
}
}

157
config_test.go Normal file
View File

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

View File

@ -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)
if err != nil {
return err
}

View File

@ -22,14 +22,26 @@ 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
}
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 +74,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 +98,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 +115,15 @@ 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
}
}

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

@ -65,15 +65,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:

View File

@ -10,7 +10,9 @@ d2vm build [context directory] [flags]
```
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image
@ -18,8 +20,10 @@ d2vm build [context directory] [flags]
--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

@ -10,14 +10,17 @@ d2vm convert [docker image] [flags]
```
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build
--luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted
--network-manager string Network manager to use for the image: none, netplan, ifupdown
-o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
-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,24 @@ 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.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"},
}
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 +74,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,7 +116,10 @@ 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") && 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()
@ -153,7 +197,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() {
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"
}
}

28
go.mod
View File

@ -8,24 +8,24 @@ require (
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/hetznercloud/hcloud-go v1.50.0
github.com/joho/godotenv v1.5.1
github.com/pkg/sftp v1.10.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.4
github.com/svenwiltink/sparsecat v1.0.0
go.linka.cloud/console v0.0.0-20220910100646-48f9f2b8843b
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/sys v0.7.0
golang.org/x/crypto v0.11.0
golang.org/x/sys v0.10.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/creack/pty v1.1.15 // indirect
@ -42,23 +42,23 @@ require (
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/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.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.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
golang.org/x/net v0.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
golang.org/x/net v0.12.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

43
go.sum
View File

@ -8,8 +8,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@ -37,6 +37,7 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -53,8 +54,8 @@ github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMd
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/hetznercloud/hcloud-go v1.50.0 h1:vS9tJvmSRwgDpMLmPnThGN87Rz8OMP3D4M3rWm8QHEQ=
github.com/hetznercloud/hcloud-go v1.50.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -78,8 +79,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
@ -101,12 +102,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -128,8 +132,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/svenwiltink/sparsecat v1.0.0 h1:SBDEIImxhD//8MskqodFR9VcixWKkZAPAW35nmA4vcw=
github.com/svenwiltink/sparsecat v1.0.0/go.mod h1:TdtvJbeTZcd+3cMQpttW6MJl/iPGZT0GHmckep0hoxU=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
@ -147,15 +152,15 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
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/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -171,15 +176,15 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
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/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=

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 {
return nil, fmt.Errorf("grub (efi) is not supported for CentOS, use grub-bios instead")
}
return grub{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubProvider) Name() string {
return "grub"
}
func init() {
RegisterBootloaderProvider(grubProvider{})
}

65
grub_bios.go 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 {
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"
@ -107,40 +103,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,6 +197,10 @@ 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" {

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

@ -2,8 +2,7 @@ FROM {{ .Image }}
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,21 @@ 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
{{- end }}

View File

@ -7,18 +7,23 @@ RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
RUN yum update -y
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
RUN yum install -y \
kernel \
systemd \
NetworkManager \
{{- if .GrubBIOS }}
grub2 \
{{- end }}
{{- if .GrubEFI }}
grub2 grub2-efi-x64 grub2-efi-x64-modules \
{{- end }}
e2fsprogs \
sudo && \
systemctl enable NetworkManager && \
systemctl unmask systemd-remount-fs.service && \
systemctl unmask getty.target && \
cd /boot && \
ln -s $(find . -name 'vmlinuz-*') vmlinuz && \
ln -s $(find . -name 'initramfs-*.img') initrd.img
find /boot -type l -exec rm {} \;
{{ if .Luks }}
RUN yum install -y cryptsetup && \
@ -28,3 +33,12 @@ 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') /boot/initrd.img
{{- end }}
RUN yum clean all && \
rm -rf /var/cache/yum

View File

@ -2,13 +2,33 @@ FROM {{ .Image }}
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 \
{{- if .Grub }}
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-${ARCH}-bin \
{{- end }}
dbus \
iproute2 \
isc-dhcp-client \
@ -48,3 +68,12 @@ 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 $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@ -2,16 +2,28 @@ FROM {{ .Image }}
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 {} \;
RUN systemctl preset-all
@ -45,3 +57,12 @@ 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 $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*

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"