mirror of https://github.com/linka-cloud/d2vm.git
Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
Adphi | f711f8919d | |
Spencer von der Ohe | b27add5767 | |
Adphi | 1934915ae8 | |
Adphi | d54b3f9a2c | |
Adphi | f8fc729486 | |
Adphi | a41bbdb745 | |
Adphi | d4c3476031 | |
Adphi | fb3ee62962 | |
Adphi | 384a4e436c | |
Adphi | a22bf9caf1 | |
Adphi | 4e533b8044 | |
Adphi | a003e176f5 |
|
@ -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
|
||||
|
|
|
@ -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-*
|
||||
|
|
|
@ -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 && \
|
||||
|
|
13
Makefile
13
Makefile
|
@ -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
|
||||
|
|
12
README.md
12
README.md
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
238
builder.go
238
builder.go
|
@ -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)
|
||||
}
|
||||
|
|
160
builder_test.go
160
builder_test.go
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
28
go.mod
|
@ -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
43
go.sum
|
@ -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=
|
||||
|
|
|
@ -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{})
|
||||
}
|
|
@ -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{})
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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{})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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{})
|
||||
}
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/*
|
||||
|
|
|
@ -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/*
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue