diff --git a/Dockerfile b/Dockerfile index 8bb0bc8..ef36b3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ RUN apt-get update && \ mount \ tar \ extlinux \ + grub2 \ cryptsetup-bin \ qemu-utils && \ apt-get clean && \ diff --git a/bootloader.go b/bootloader.go index b4fa6b8..afc24ae 100644 --- a/bootloader.go +++ b/bootloader.go @@ -33,10 +33,10 @@ func BootloaderByName(name string) (BootloaderProvider, error) { } type BootloaderProvider interface { - New(c Config) (Bootloader, error) + New(c Config, r OSRelease) (Bootloader, error) Name() string } type Bootloader interface { - Setup(ctx context.Context, raw, path, cmdline string) error + Setup(ctx context.Context, dev, root, cmdline string) error } diff --git a/builder.go b/builder.go index c470405..55e4066 100644 --- a/builder.go +++ b/builder.go @@ -127,7 +127,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, if err != nil { return nil, err } - bl, err := blp.New(config) + bl, err := blp.New(config, osRelease) if err != nil { return nil, err } @@ -200,7 +200,7 @@ 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 { @@ -452,9 +452,9 @@ func (b *builder) cmdline(_ context.Context) string { } } -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) 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 { diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 01f0a74..40a8ea3 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -110,6 +110,7 @@ var ( d2vm.WithOutput(output), d2vm.WithCmdLineExtra(cmdLineExtra), d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), + d2vm.WithBootLoader(bootloader), d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index 081a3fb..1aea72a 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -98,6 +98,7 @@ var ( d2vm.WithOutput(output), d2vm.WithCmdLineExtra(cmdLineExtra), d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), + d2vm.WithBootLoader(bootloader), d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go index 6c095cf..ce94e72 100644 --- a/cmd/d2vm/flags.go +++ b/cmd/d2vm/flags.go @@ -33,6 +33,7 @@ var ( containerDiskTag = "" push bool networkManager string + bootloader string splitBoot bool bootSize uint64 luksPassword string @@ -53,6 +54,7 @@ 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(&bootloader, "bootloader", "syslinux", "Bootloader to use: syslinux, grub") 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") return flags diff --git a/config_test.go b/config_test.go index 88119e8..30e9996 100644 --- a/config_test.go +++ b/config_test.go @@ -38,7 +38,7 @@ func testConfig(t *testing.T, ctx context.Context, img string, config Config) { r, err := FetchDockerImageOSRelease(ctx, img, tmpPath) require.NoError(t, err) defer docker.Remove(ctx, img) - d, err := NewDockerfile(r, img, "root", "", false) + d, err := NewDockerfile(r, img, "root", "", false, false) require.NoError(t, err) logrus.Infof("docker image based on %s", d.Release.Name) p := filepath.Join(tmpPath, docker.FormatImgName(img)) diff --git a/convert.go b/convert.go index 3209b68..cf9ff35 100644 --- a/convert.go +++ b/convert.go @@ -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.bootLoader == "grub") if err != nil { return err } @@ -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, o.bootLoader) if err != nil { return err } diff --git a/convert_options.go b/convert_options.go index 3af695d..292251b 100644 --- a/convert_options.go +++ b/convert_options.go @@ -22,6 +22,7 @@ type convertOptions struct { output string cmdLineExtra string networkManager NetworkManager + bootLoader string raw bool splitBoot bool @@ -62,6 +63,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 diff --git a/dockerfile.go b/dockerfile.go index 8bfa4df..306f412 100644 --- a/dockerfile.go +++ b/dockerfile.go @@ -65,6 +65,7 @@ type Dockerfile struct { Release OSRelease NetworkManager NetworkManager Luks bool + Grub bool tmpl *template.Template } @@ -72,8 +73,8 @@ 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, grub bool) (Dockerfile, error) { + d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, Grub: grub} var net NetworkManager switch release.ID { case ReleaseDebian: diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 65cd6aa..d1e44e0 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -66,6 +66,14 @@ func TestConvert(t *testing.T) { name: "luks", args: []string{"--luks-password=root"}, }, + { + name: "grub", + args: []string{"--bootloader=grub"}, + }, + { + name: "grub-luks", + args: []string{"--bootloader=grub", "--luks-password=root"}, + }, } for _, tt := range tests { diff --git a/grub.go b/grub.go new file mode 100644 index 0000000..86d7095 --- /dev/null +++ b/grub.go @@ -0,0 +1,105 @@ +// 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" + exec2 "os/exec" + "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 grub struct { + name string + c Config + r OSRelease +} + +func (g grub) Setup(ctx context.Context, dev, root string, cmdline string) error { + logrus.Infof("setting up grub bootloader") + if err := os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil { + return err + } + if err := os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil { + return err + } + mounts := []string{"dev", "proc", "sys"} + var unmounts []string + defer 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) + } + } + }() + for _, v := range mounts { + if err := exec.Run(ctx, "mount", "-o", "bind", "/"+v, filepath.Join(root, v)); err != nil { + return err + } + unmounts = append(unmounts, v) + } + + if err := exec.Run(ctx, "chroot", root, g.name+"-install", "--target=i386-pc", "--boot-directory", "/boot", dev); err != nil { + return err + } + if err := exec.Run(ctx, "chroot", root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg"); err != nil { + return err + } + return nil +} + +type grubBootloaderProvider struct { + config Config +} + +func (g grubBootloaderProvider) New(c Config, r OSRelease) (Bootloader, error) { + name := "grub" + if r.ID == "centos" { + name = "grub2" + } + if _, err := exec2.LookPath("grub-install"); err != nil { + return nil, err + } + if _, err := exec2.LookPath("grub-mkconfig"); err != nil { + return nil, err + } + return grub{ + name: name, + c: c, + r: r, + }, nil +} + +func (g grubBootloaderProvider) Name() string { + return "grub" +} + +func init() { + RegisterBootloaderProvider(grubBootloaderProvider{}) +} diff --git a/syslinux.go b/syslinux.go index 3b6a509..0b027fc 100644 --- a/syslinux.go +++ b/syslinux.go @@ -50,15 +50,16 @@ type syslinux struct { 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 { +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(path, "syslinux.cfg"), []byte(fmt.Sprintf(syslinuxCfg, s.c.Kernel, cmdline)), perm); err != nil { + 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", raw), "bs=440", "count=1", "conv=notrunc"); err != nil { + 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 @@ -66,7 +67,7 @@ func (s syslinux) Setup(ctx context.Context, raw, path string, cmdline string) e type syslinuxProvider struct{} -func (s syslinuxProvider) New(c Config) (Bootloader, error) { +func (s syslinuxProvider) New(c Config, _ OSRelease) (Bootloader, error) { mbrBin := "" for _, v := range mbrPaths { if _, err := os.Stat(v); err == nil { diff --git a/templates/alpine.Dockerfile b/templates/alpine.Dockerfile index fb675a1..843c47d 100644 --- a/templates/alpine.Dockerfile +++ b/templates/alpine.Dockerfile @@ -36,3 +36,7 @@ RUN apk add --no-cache cryptsetup && \ echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \ mkinitfs $(ls /lib/modules) {{- end }} + +{{- if .Grub }} \ +RUN apk add --no-cache grub grub-bios +{{- end }} diff --git a/templates/centos.Dockerfile b/templates/centos.Dockerfile index 77a699a..bf23f99 100644 --- a/templates/centos.Dockerfile +++ b/templates/centos.Dockerfile @@ -11,6 +11,9 @@ RUN yum install -y \ kernel \ systemd \ NetworkManager \ +{{- if .Grub }} + grub2 \ +{{- end }} e2fsprogs \ sudo && \ systemctl enable NetworkManager && \ diff --git a/templates/debian.Dockerfile b/templates/debian.Dockerfile index bcb5062..396cec7 100644 --- a/templates/debian.Dockerfile +++ b/templates/debian.Dockerfile @@ -9,6 +9,9 @@ RUN apt-get -y update && \ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ systemd-sysv \ systemd \ + {{- if .Grub }} + grub2 \ + {{- end }} dbus \ iproute2 \ isc-dhcp-client \ diff --git a/templates/ubuntu.Dockerfile b/templates/ubuntu.Dockerfile index 1cfe6c4..b826d06 100644 --- a/templates/ubuntu.Dockerfile +++ b/templates/ubuntu.Dockerfile @@ -8,6 +8,9 @@ RUN apt-get update -y && \ initramfs-tools \ systemd-sysv \ systemd \ +{{- if .Grub }} + grub2 \ +{{- end }} dbus \ isc-dhcp-client \ iproute2 \