From 532ee3f1a36b0311ffcf30041f5dbdbb473de109 Mon Sep 17 00:00:00 2001 From: Adphi Date: Wed, 22 Feb 2023 17:26:32 +0100 Subject: [PATCH] add split boot partiton support Signed-off-by: Adphi --- Dockerfile | 4 +- README.md | 4 + builder.go | 158 ++++++++++++++++++++----- cmd/d2vm/build.go | 2 + cmd/d2vm/convert.go | 6 +- cmd/d2vm/flags.go | 4 + convert.go | 2 +- convert_options.go | 19 ++- docs/content/reference/d2vm_build.md | 2 + docs/content/reference/d2vm_convert.md | 2 + templates/alpine.Dockerfile | 6 +- 11 files changed, 168 insertions(+), 41 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4f2fce1..a94eefe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,9 @@ RUN apt-get update && \ mount \ tar \ extlinux \ - qemu-utils + qemu-utils && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ diff --git a/README.md b/README.md index 52ee446..be3225d 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Usage: Flags: --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one + --boot-size uint Size of the boot partition in MB (default 100) --force Override output qcow2 image -h, --help help for convert --network-manager string Network manager to use for the image: none, netplan, ifupdown @@ -163,6 +164,7 @@ Flags: --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") + --split-boot Split the boot partition from the root partition -t, --tag string Container disk Docker image tag Global Flags: @@ -305,6 +307,7 @@ Usage: Flags: --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one + --boot-size uint Size of the boot partition in MB (default 100) --build-arg stringArray Set build-time variables -f, --file string Name of the Dockerfile --force Override output qcow2 image @@ -315,6 +318,7 @@ Flags: --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") + --split-boot Split the boot partition from the root partition -t, --tag string Container disk Docker image tag Global Flags: diff --git a/builder.go b/builder.go index 0fada63..3ce7600 100644 --- a/builder.go +++ b/builder.go @@ -121,18 +121,24 @@ type builder struct { diskOut string format string - size int64 + size uint64 mntPoint string + splitBoot bool + bootSize uint64 + mbrPath string - loDevice string - loPart string - diskUUD string + loDevice string + bootPart string + rootPart string + bootUUID string + rootUUID string + cmdLineExtra string } -func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, osRelease OSRelease, format string, cmdLineExtra string) (Builder, error) { +func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64) (Builder, error) { if err := checkDependencies(); err != nil { return nil, err } @@ -147,6 +153,14 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, o return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " ")) } + if splitBoot && bootSize < 50 { + return nil, fmt.Errorf("boot partition size must be at least 50MiB") + } + + if splitBoot && bootSize >= size { + 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 { @@ -158,7 +172,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, o return nil, fmt.Errorf("unable to find syslinux's mbr.bin path") } if size == 0 { - size = 10 * int64(datasize.GB) + size = 10 * uint64(datasize.GB) } if disk == "" { disk = "disk0" @@ -186,6 +200,8 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, o mbrPath: mbrBin, mntPoint: filepath.Join(workdir, "/mnt"), cmdLineExtra: cmdLineExtra, + splitBoot: splitBoot, + bootSize: bootSize, } if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil { return nil, err @@ -252,9 +268,21 @@ func (b *builder) makeImg(ctx context.Context) error { return err } - if err := exec.Run(ctx, "parted", "-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"); err != nil { + var args []string + if b.splitBoot { + args = []string{"-s", b.diskRaw, + "mklabel", "msdos", "mkpart", "primary", "1Mib", fmt.Sprintf("%dMib", b.bootSize), + "mkpart", "primary", fmt.Sprintf("%dMib", b.bootSize), "100%", + "set", "1", "boot", "on", + } + } else { + args = []string{"-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"} + } + + if err := exec.Run(ctx, "parted", args...); err != nil { return err } + return nil } @@ -268,12 +296,29 @@ func (b *builder) mountImg(ctx context.Context) error { if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil { return err } - b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice)) + b.bootPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice)) + if b.splitBoot { + b.rootPart = fmt.Sprintf("/dev/mapper/%sp2", filepath.Base(b.loDevice)) + } else { + b.rootPart = b.bootPart + } logrus.Infof("creating raw image file system") - if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil { + if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { return err } - if err := exec.Run(ctx, "mount", b.loPart, b.mntPoint); err != nil { + if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { + return err + } + if !b.splitBoot { + return nil + } + if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil { + return err + } + if err := exec.Run(ctx, "mkfs.ext4", b.bootPart); err != nil { + return err + } + if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil { return err } return nil @@ -282,6 +327,11 @@ func (b *builder) mountImg(ctx context.Context) error { func (b *builder) unmountImg(ctx context.Context) error { logrus.Infof("unmounting raw image") var merr error + if b.splitBoot { + if err := exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")); err != nil { + merr = multierr.Append(merr, err) + } + } if err := exec.Run(ctx, "umount", b.mntPoint); err != nil { merr = multierr.Append(merr, err) } @@ -302,14 +352,28 @@ func (b *builder) copyRootFS(ctx context.Context) error { return nil } -func (b *builder) setupRootFS(ctx context.Context) error { - logrus.Infof("setting up rootfs") - o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", b.loPart) +func diskUUID(ctx context.Context, disk string) (string, error) { + o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", disk) if err != nil { - return err + return "", err + } + return strings.TrimSuffix(o, "\n"), nil +} + +func (b *builder) setupRootFS(ctx context.Context) (err error) { + logrus.Infof("setting up rootfs") + b.rootUUID, err = diskUUID(ctx, b.rootPart) + var fstab string + if b.splitBoot { + b.bootUUID, err = diskUUID(ctx, b.bootPart) + if err != nil { + 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) + } else { + b.bootUUID = b.rootUUID + fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID) } - b.diskUUD = strings.TrimSuffix(o, "\n") - fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD) if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil { return err } @@ -329,21 +393,51 @@ func (b *builder) setupRootFS(ctx context.Context) error { if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil { return err } - if b.osRelease.ID != ReleaseAlpine { + // 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")) + if err != nil { + return err + } + by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...) + if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil { + return err + } + if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil { + return err + } + return nil + 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 } - by, err := os.ReadFile(b.chPath("/etc/inittab")) - if err != nil { - return err - } - by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...) - if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil { - return err - } - if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil { - return err - } - return nil } func (b *builder) installKernel(ctx context.Context) error { @@ -355,7 +449,7 @@ func (b *builder) installKernel(ctx context.Context) error { if err != nil { return err } - if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD, b.cmdLineExtra), perm); err != nil { + if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra), perm); err != nil { return err } return nil @@ -393,13 +487,13 @@ func (b *builder) Close() error { return b.img.Close() } -func block(path string, size int64) error { +func block(path string, size uint64) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() - return f.Truncate(size) + return f.Truncate(int64(size)) } func checkDependencies() error { diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 0606e60..aa75295 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -107,6 +107,8 @@ var ( d2vm.WithCmdLineExtra(cmdLineExtra), d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), d2vm.WithRaw(raw), + d2vm.WithSplitBoot(splitBoot), + d2vm.WithBootSize(bootSize), ); err != nil { return err } diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index a335a3f..b231699 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -95,6 +95,8 @@ var ( d2vm.WithCmdLineExtra(cmdLineExtra), d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), d2vm.WithRaw(raw), + d2vm.WithSplitBoot(splitBoot), + d2vm.WithBootSize(bootSize), ); err != nil { return err } @@ -109,12 +111,12 @@ var ( } ) -func parseSize(s string) (int64, error) { +func parseSize(s string) (uint64, error) { var v datasize.ByteSize if err := v.UnmarshalText([]byte(s)); err != nil { return 0, err } - return int64(v), nil + return uint64(v), nil } func init() { diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go index 46047bf..d64a900 100644 --- a/cmd/d2vm/flags.go +++ b/cmd/d2vm/flags.go @@ -33,6 +33,8 @@ var ( containerDiskTag = "" push bool networkManager string + splitBoot bool + bootSize uint64 ) func buildFlags() *pflag.FlagSet { @@ -46,5 +48,7 @@ func buildFlags() *pflag.FlagSet { flags.BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more") flags.StringVarP(&containerDiskTag, "tag", "t", "", "Container disk Docker image tag") flags.BoolVar(&push, "push", false, "Push the container disk image to the registry") + flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition") + flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB") return flags } diff --git a/convert.go b/convert.go index d9f5aa6..b7f3893 100644 --- a/convert.go +++ b/convert.go @@ -79,7 +79,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) + b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize) if err != nil { return err } diff --git a/convert_options.go b/convert_options.go index bdb0cd2..4bc4533 100644 --- a/convert_options.go +++ b/convert_options.go @@ -17,15 +17,18 @@ package d2vm type ConvertOption func(o *convertOptions) type convertOptions struct { - size int64 + size uint64 password string output string cmdLineExtra string networkManager NetworkManager raw bool + + splitBoot bool + bootSize uint64 } -func WithSize(size int64) ConvertOption { +func WithSize(size uint64) ConvertOption { return func(o *convertOptions) { o.size = size } @@ -60,3 +63,15 @@ func WithRaw(raw bool) ConvertOption { o.raw = raw } } + +func WithSplitBoot(b bool) ConvertOption { + return func(o *convertOptions) { + o.splitBoot = b + } +} + +func WithBootSize(bootSize uint64) ConvertOption { + return func(o *convertOptions) { + o.bootSize = bootSize + } +} diff --git a/docs/content/reference/d2vm_build.md b/docs/content/reference/d2vm_build.md index 0a274f2..34d3241 100644 --- a/docs/content/reference/d2vm_build.md +++ b/docs/content/reference/d2vm_build.md @@ -10,6 +10,7 @@ d2vm build [context directory] [flags] ``` --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one + --boot-size uint Size of the boot partition in MB (default 100) --build-arg stringArray Set build-time variables -f, --file string Name of the Dockerfile --force Override output qcow2 image @@ -20,6 +21,7 @@ d2vm build [context directory] [flags] --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") + --split-boot Split the boot partition from the root partition -t, --tag string Container disk Docker image tag ``` diff --git a/docs/content/reference/d2vm_convert.md b/docs/content/reference/d2vm_convert.md index e02a726..7624b30 100644 --- a/docs/content/reference/d2vm_convert.md +++ b/docs/content/reference/d2vm_convert.md @@ -10,6 +10,7 @@ d2vm convert [docker image] [flags] ``` --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one + --boot-size uint Size of the boot partition in MB (default 100) --force Override output qcow2 image -h, --help help for convert --network-manager string Network manager to use for the image: none, netplan, ifupdown @@ -19,6 +20,7 @@ d2vm convert [docker image] [flags] --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") + --split-boot Split the boot partition from the root partition -t, --tag string Container disk Docker image tag ``` diff --git a/templates/alpine.Dockerfile b/templates/alpine.Dockerfile index 0727b02..7f48e20 100644 --- a/templates/alpine.Dockerfile +++ b/templates/alpine.Dockerfile @@ -6,14 +6,14 @@ RUN apk update --no-cache && \ apk add \ util-linux \ linux-virt \ -{{ if ge .Release.VersionID "3.17" }} \ +{{- if ge .Release.VersionID "3.17" }} busybox-openrc \ busybox-mdev-openrc \ busybox-extras-openrc \ busybox-mdev-openrc \ -{{ else }} +{{- else }} busybox-initscripts \ -{{ end }} +{{- end }} openrc RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done