From a003e176f5e9a4c8b63325152a5f45b25b1d525d Mon Sep 17 00:00:00 2001 From: Adphi Date: Mon, 11 Sep 2023 13:44:12 +0200 Subject: [PATCH] chore: bootloader abtraction Signed-off-by: Adphi --- .gitignore | 2 + Makefile | 4 +- bootloader.go | 42 +++++++++ builder.go | 136 +++++++----------------------- config.go | 89 +++++++++++++++++++ builder_test.go => config_test.go | 98 ++++++++------------- convert.go | 2 +- syslinux.go | 92 ++++++++++++++++++++ 8 files changed, 292 insertions(+), 173 deletions(-) create mode 100644 bootloader.go create mode 100644 config.go rename builder_test.go => config_test.go (52%) create mode 100644 syslinux.go diff --git a/.gitignore b/.gitignore index 81a1ce9..231821d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ dist/ images /d2vm /examples/build +/examples/full/demo-magic +/examples/full/inside .goreleaser.yaml docs/build docs-src diff --git a/Makefile b/Makefile index c429ae7..9bc3393 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,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 +116,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 diff --git a/bootloader.go b/bootloader.go new file mode 100644 index 0000000..b4fa6b8 --- /dev/null +++ b/bootloader.go @@ -0,0 +1,42 @@ +// 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) (Bootloader, error) + Name() string +} + +type Bootloader interface { + Setup(ctx context.Context, raw, path, cmdline string) error +} diff --git a/builder.go b/builder.go index bc0fa75..c470405 100644 --- a/builder.go +++ b/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", "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 @@ -128,8 +68,6 @@ type builder struct { splitBoot bool bootSize uint64 - mbrPath string - loDevice string bootPart string rootPart string @@ -145,7 +83,7 @@ type builder struct { cmdLineExtra 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) { +func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string, bootLoader string) (Builder, error) { if err := checkDependencies(); err != nil { return nil, err } @@ -176,16 +114,24 @@ 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 } + + blp, err := BootloaderByName(bootLoader) + if err != nil { + return nil, err + } + bl, err := blp.New(config) + if err != nil { + return nil, err + } + if size == 0 { size = 10 * uint64(datasize.GB) } @@ -207,12 +153,13 @@ 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, @@ -259,9 +206,6 @@ func (b *builder) Build(ctx context.Context) (err error) { 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 } @@ -490,45 +434,27 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { } } -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 - } - sysconfig, err := sysconfig(b.osRelease) - if err != nil { - return err - } - var cfg string +func (b *builder) cmdline(_ context.Context) 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) + return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) 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)) + 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 - 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) + return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) } } else { - cfg = fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra) + return b.config.Cmdline(RootUUID(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) installKernel(ctx context.Context) error { + logrus.Infof("installing linux kernel") + return b.bootloader.Setup(ctx, b.diskRaw, b.chPath("/boot"), b.cmdline(ctx)) } func (b *builder) convert2Img(ctx context.Context) error { diff --git a/config.go b/config.go new file mode 100644 index 0000000..88aad53 --- /dev/null +++ b/config.go @@ -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: "/vmlinuz", + Initrd: "/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) + + } +} diff --git a/builder_test.go b/config_test.go similarity index 52% rename from builder_test.go rename to config_test.go index 571a46c..88119e8 100644 --- a/builder_test.go +++ b/config_test.go @@ -23,14 +23,13 @@ import ( "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) { +func testConfig(t *testing.T, ctx context.Context, img string, config Config) { 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)) @@ -39,9 +38,6 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init 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) @@ -55,95 +51,67 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init 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)) + require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel)) + require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd)) } -func TestSyslinuxCfg(t *testing.T) { +func TestConfig(t *testing.T) { t.Parallel() tests := []struct { - image string - kernel string - initrd string - sysconfig string + image string + config Config }{ { - image: "ubuntu:18.04", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "ubuntu:18.04", + config: configDebian, }, { - image: "ubuntu:20.04", - kernel: "/boot/vmlinuz", - initrd: "/boot/initrd.img", - sysconfig: syslinuxCfgUbuntu, + image: "ubuntu:20.04", + config: configUbuntu, }, { - image: "ubuntu:22.04", - kernel: "/boot/vmlinuz", - initrd: "/boot/initrd.img", - sysconfig: syslinuxCfgUbuntu, + image: "ubuntu:22.04", + config: configUbuntu, }, { - image: "ubuntu:latest", - kernel: "/boot/vmlinuz", - initrd: "/boot/initrd.img", - sysconfig: syslinuxCfgUbuntu, + image: "ubuntu:latest", + config: configUbuntu, }, { - image: "debian:9", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "debian:9", + config: configDebian, }, { - image: "debian:10", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "debian:10", + config: configDebian, }, { - image: "debian:11", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "debian:11", + config: configDebian, }, { - image: "debian:latest", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "debian:latest", + config: configDebian, }, { - image: "kalilinux/kali-rolling:latest", - kernel: "/vmlinuz", - initrd: "/initrd.img", - sysconfig: syslinuxCfgDebian, + image: "kalilinux/kali-rolling:latest", + config: configDebian, }, { - image: "alpine:3.16", - kernel: "/boot/vmlinuz-virt", - initrd: "/boot/initramfs-virt", - sysconfig: syslinuxCfgAlpine, + image: "alpine:3.16", + config: configAlpine, }, { - image: "alpine", - kernel: "/boot/vmlinuz-virt", - initrd: "/boot/initramfs-virt", - sysconfig: syslinuxCfgAlpine, + image: "alpine", + config: configAlpine, }, { - image: "centos:8", - kernel: "/boot/vmlinuz", - initrd: "/boot/initrd.img", - sysconfig: syslinuxCfgCentOS, + image: "centos:8", + config: configCentOS, }, { - image: "centos:latest", - kernel: "/boot/vmlinuz", - initrd: "/boot/initrd.img", - sysconfig: syslinuxCfgCentOS, + image: "centos:latest", + config: configCentOS, }, } exec.SetDebug(true) @@ -154,7 +122,7 @@ func TestSyslinuxCfg(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) defer cancel() - testSysconfig(t, ctx, test.image, test.sysconfig, test.kernel, test.initrd) + testConfig(t, ctx, test.image, test.config) }) } } diff --git a/convert.go b/convert.go index 361599d..3209b68 100644 --- a/convert.go +++ b/convert.go @@ -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.bootSize, o.luksPassword, "") if err != nil { return err } diff --git a/syslinux.go b/syslinux.go new file mode 100644 index 0000000..3b6a509 --- /dev/null +++ b/syslinux.go @@ -0,0 +1,92 @@ +// 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) Setup(ctx context.Context, raw, path string, cmdline string) error { + if err := exec.Run(ctx, "extlinux", "--install", path); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(path, "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", raw), "bs=440", "count=1", "conv=notrunc"); err != nil { + return err + } + return nil +} + +type syslinuxProvider struct{} + +func (s syslinuxProvider) New(c Config) (Bootloader, error) { + 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{}) +}