From 31a00ef73a78f09a38225018d4a65b1da6ba6f5c Mon Sep 17 00:00:00 2001 From: Andrew Rynhard Date: Mon, 18 Mar 2019 14:01:58 -0700 Subject: [PATCH] feat: install bootloader to block device (#455) Signed-off-by: Andrew Rynhard --- Dockerfile | 8 +- .../internal/platform/baremetal/baremetal.go | 13 ++- .../app/init/internal/rootfs/mount/mount.go | 2 + internal/app/osinstall/cmd/install.go | 7 +- .../pkg/blockdevice/bootloader/bootloader.go | 11 +++ .../bootloader/syslinux/syslinux.go | 93 +++++++++++++++++++ .../blockdevice/filesystem/vfat/superblock.go | 2 +- .../pkg/blockdevice/filesystem/vfat/vfat.go | 2 +- internal/pkg/blockdevice/table/gpt/gpt.go | 7 +- .../table/gpt/partition/options.go | 16 +++- internal/pkg/install/install.go | 12 ++- internal/pkg/install/prepare.go | 26 +++++- internal/pkg/kernel/kernel.go | 12 ++- 13 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 internal/pkg/blockdevice/bootloader/bootloader.go create mode 100644 internal/pkg/blockdevice/bootloader/syslinux/syslinux.go diff --git a/Dockerfile b/Dockerfile index b594239ed..09ebf811a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,9 @@ RUN ln -s /toolchain/etc/ssl/certs/ca-certificates /etc/ssl/certs/ca-certificate # fhs COPY hack/scripts/fhs.sh /bin RUN fhs.sh /rootfs +# ca-certificates +RUN mkdir -p /rootfs/etc/ssl/certs +RUN curl -o /rootfs/etc/ssl/certs/ca-certificates.crt https://curl.haxx.se/ca/cacert.pem # xfsprogs WORKDIR /tmp/xfsprogs RUN curl -L https://www.kernel.org/pub/linux/utils/fs/xfs/xfsprogs/xfsprogs-4.18.0.tar.xz | tar -xJ --strip-components=1 @@ -66,7 +69,7 @@ RUN curl -L https://www.kernel.org/pub/linux/utils/boot/syslinux/syslinux-6.03.t RUN ln -s /toolchain/bin/pwd /bin/pwd && \ make installer && \ cp /tmp/syslinux/bios/extlinux/extlinux /rootfs/bin && \ - cp /tmp/syslinux/bios/mbr/gptmbr.bin /rootfs/share + cp /tmp/syslinux/efi64/mbr/gptmbr.bin /rootfs/share # golang ENV GOROOT /toolchain/usr/local/go ENV GOPATH /toolchain/go @@ -157,9 +160,6 @@ RUN ../configure \ RUN make -j $(($(nproc) / 2)) RUN make install DESTDIR=/rootfs RUN make install DESTDIR=/toolchain -# ca-certificates -RUN mkdir -p /rootfs/etc/ssl/certs -RUN curl -o /rootfs/etc/ssl/certs/ca-certificates.crt https://curl.haxx.se/ca/cacert.pem # crictl RUN curl -L https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.13.0/crictl-v1.13.0-linux-amd64.tar.gz | tar -xz -C /rootfs/bin # containerd diff --git a/internal/app/init/internal/platform/baremetal/baremetal.go b/internal/app/init/internal/platform/baremetal/baremetal.go index ddc7ebcac..03e6ba705 100644 --- a/internal/app/init/internal/platform/baremetal/baremetal.go +++ b/internal/app/init/internal/platform/baremetal/baremetal.go @@ -83,6 +83,15 @@ func (b *BareMetal) Prepare(data *userdata.UserData) (err error) { // Install provides the functionality to install talos by // download the necessary bits and write them to a target device // nolint: gocyclo, dupl -func (b *BareMetal) Install(data *userdata.UserData) error { - return install.Install(data) +func (b *BareMetal) Install(data *userdata.UserData) (err error) { + var cmdlineBytes []byte + cmdlineBytes, err = kernel.ReadProcCmdline() + if err != nil { + return err + } + if err = install.Install(string(cmdlineBytes), data); err != nil { + return errors.Wrap(err, "failed to install to bare metal") + } + + return nil } diff --git a/internal/app/init/internal/rootfs/mount/mount.go b/internal/app/init/internal/rootfs/mount/mount.go index 299a4dd1d..98535023a 100644 --- a/internal/app/init/internal/rootfs/mount/mount.go +++ b/internal/app/init/internal/rootfs/mount/mount.go @@ -5,6 +5,7 @@ package mount import ( + "log" "os" "path" @@ -237,6 +238,7 @@ func mountpoints() (mountpoints *mount.Points, err error) { if dev, err = probe.GetDevWithFileSystemLabel(name); err != nil { if name == constants.BootPartitionLabel { // A bootloader is not always required. + log.Println("WARNING: no ESP partition was found") continue } return nil, errors.Errorf("failed to find device with label %s: %v", name, err) diff --git a/internal/app/osinstall/cmd/install.go b/internal/app/osinstall/cmd/install.go index 92ee1e063..f3a69772a 100644 --- a/internal/app/osinstall/cmd/install.go +++ b/internal/app/osinstall/cmd/install.go @@ -14,6 +14,10 @@ import ( "github.com/spf13/cobra" ) +var ( + kernelParams string +) + // installCmd reads in a userData file and attempts to parse it var installCmd = &cobra.Command{ Use: "install", @@ -38,7 +42,7 @@ var installCmd = &cobra.Command{ log.Fatal(err) } - err = install.Install(ud) + err = install.Install(kernelParams, ud) if err != nil { log.Fatal(err) } @@ -46,6 +50,7 @@ var installCmd = &cobra.Command{ } func init() { + installCmd.Flags().StringVarP(&kernelParams, "kernel-parameters", "k", "", "kernel parameter flags") installCmd.Flags().StringVarP(&userdataFile, "userdata", "u", "", "path or url of userdata file") rootCmd.AddCommand(installCmd) } diff --git a/internal/pkg/blockdevice/bootloader/bootloader.go b/internal/pkg/blockdevice/bootloader/bootloader.go new file mode 100644 index 000000000..bfb07f4de --- /dev/null +++ b/internal/pkg/blockdevice/bootloader/bootloader.go @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package bootloader + +// Bootloader describes a bootloader. +type Bootloader interface { + Prepare(string) error + Install(string) error +} diff --git a/internal/pkg/blockdevice/bootloader/syslinux/syslinux.go b/internal/pkg/blockdevice/bootloader/syslinux/syslinux.go new file mode 100644 index 000000000..a5b8d40ec --- /dev/null +++ b/internal/pkg/blockdevice/bootloader/syslinux/syslinux.go @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package syslinux + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "os/exec" + "text/template" + + "github.com/autonomy/talos/internal/pkg/constants" + "github.com/autonomy/talos/internal/pkg/version" +) + +const extlinuxConfig = `DEFAULT Talos + SAY Talos ({{ .Version }}) by Autonomy +LABEL Talos + KERNEL /vmlinuz + INITRD /initramfs.xz + APPEND {{ .Append }}` + +const gptmbrbin = "/usr/share/gptmbr.bin" + +// Syslinux represents the syslinux bootloader. +type Syslinux struct{} + +// Prepare implements the Bootloader interface. It works by invoking writing +// gptmbr.bin to a block device. +func Prepare(dev string) (err error) { + b, err := ioutil.ReadFile(gptmbrbin) + if err != nil { + return err + } + f, err := os.OpenFile(dev, os.O_WRONLY, os.ModeDevice) + if err != nil { + return err + } + // nolint: errcheck + defer f.Close() + if _, err := f.Write(b); err != nil { + return err + } + + return nil +} + +// Install implements the Bootloader interface. It sets up extlinux with the +// specified kernel parameters. +func Install(args string) (err error) { + aux := struct { + Version string + Append string + }{ + Version: version.Tag, + Append: args, + } + + b := []byte{} + wr := bytes.NewBuffer(b) + t := template.Must(template.New("extlinux").Parse(extlinuxConfig)) + if err = t.Execute(wr, aux); err != nil { + return err + } + + if err = os.MkdirAll(constants.NewRoot+"/boot/extlinux", os.ModeDir); err != nil { + return err + } + + log.Println("writing extlinux.conf to disk") + if err = ioutil.WriteFile(constants.NewRoot+"/boot/extlinux/extlinux.conf", wr.Bytes(), 0600); err != nil { + return err + } + + if err = cmd("extlinux", "--install", constants.NewRoot+"/boot/extlinux"); err != nil { + return err + } + + return nil +} + +func cmd(name string, args ...string) error { + cmd := exec.Command(name, args...) + err := cmd.Start() + if err != nil { + return err + } + + return cmd.Wait() +} diff --git a/internal/pkg/blockdevice/filesystem/vfat/superblock.go b/internal/pkg/blockdevice/filesystem/vfat/superblock.go index 22425c77c..dc331688b 100644 --- a/internal/pkg/blockdevice/filesystem/vfat/superblock.go +++ b/internal/pkg/blockdevice/filesystem/vfat/superblock.go @@ -57,5 +57,5 @@ func (sb *SuperBlock) Offset() int64 { // Type implements the SuperBlocker interface. func (sb *SuperBlock) Type() string { - return "fat32" + return "vfat" } diff --git a/internal/pkg/blockdevice/filesystem/vfat/vfat.go b/internal/pkg/blockdevice/filesystem/vfat/vfat.go index 4ceaa0089..ae342ea04 100644 --- a/internal/pkg/blockdevice/filesystem/vfat/vfat.go +++ b/internal/pkg/blockdevice/filesystem/vfat/vfat.go @@ -15,7 +15,7 @@ func MakeFS(partname string, setters ...Option) error { args := []string{} if opts.Label != "" { - args = append(args, "-n", opts.Label) + args = append(args, "-F", "32", "-n", opts.Label) } args = append(args, partname) diff --git a/internal/pkg/blockdevice/table/gpt/gpt.go b/internal/pkg/blockdevice/table/gpt/gpt.go index 0b75519c1..f427a288e 100644 --- a/internal/pkg/blockdevice/table/gpt/gpt.go +++ b/internal/pkg/blockdevice/table/gpt/gpt.go @@ -285,10 +285,9 @@ func (gpt *GPT) Add(size uint64, setters ...interface{}) (table.Partition, error ID: uuid, FirstLBA: start, LastLBA: end, - // TODO(andrewrynhard): Flags should be an option. - Flags: 0, - Name: opts.Name, - Number: int32(len(gpt.partitions) + 1), + Flags: opts.Flags, + Name: opts.Name, + Number: int32(len(gpt.partitions) + 1), } gpt.partitions = append(gpt.partitions, partition) diff --git a/internal/pkg/blockdevice/table/gpt/partition/options.go b/internal/pkg/blockdevice/table/gpt/partition/options.go index e18f85461..bb7f7f15f 100644 --- a/internal/pkg/blockdevice/table/gpt/partition/options.go +++ b/internal/pkg/blockdevice/table/gpt/partition/options.go @@ -10,9 +10,10 @@ import ( // Options is the functional options struct. type Options struct { - Type uuid.UUID - Name string - Test bool + Type uuid.UUID + Name string + Flags uint64 + Test bool } // Option is the functional option func. @@ -35,6 +36,15 @@ func WithPartitionName(o string) Option { } } +// WithLegacyBIOSBootableAttribute marks the partition as bootable. +func WithLegacyBIOSBootableAttribute(o bool) Option { + return func(args *Options) { + if o == true { + args.Flags = 4 + } + } +} + // WithPartitionTest allows us to disable the IsNew partition // check. This is only intended to be used for tests. func WithPartitionTest(t bool) Option { diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go index c5ae32305..3a2a68acb 100644 --- a/internal/pkg/install/install.go +++ b/internal/pkg/install/install.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strings" + "github.com/autonomy/talos/internal/pkg/blockdevice/bootloader/syslinux" "github.com/autonomy/talos/internal/pkg/constants" "github.com/autonomy/talos/internal/pkg/userdata" "github.com/pkg/errors" @@ -24,7 +25,7 @@ import ( // Install fetches the necessary data locations and copies or extracts // to the target locations // nolint: gocyclo -func Install(data *userdata.UserData) (err error) { +func Install(args string, data *userdata.UserData) (err error) { if data.Install == nil { return nil } @@ -46,6 +47,10 @@ func Install(data *userdata.UserData) (err error) { previousMountPoint = dest } + if err = os.MkdirAll(dest, os.ModeDir); err != nil { + return err + } + // Extract artifact if necessary, otherwise place at root of partition/filesystem for _, artifact := range urls { switch { @@ -120,6 +125,11 @@ func Install(data *userdata.UserData) (err error) { } } } + + if err = syslinux.Install(args); err != nil { + return err + } + return nil } diff --git a/internal/pkg/install/prepare.go b/internal/pkg/install/prepare.go index 9c2ae4848..6417fc383 100644 --- a/internal/pkg/install/prepare.go +++ b/internal/pkg/install/prepare.go @@ -12,6 +12,8 @@ import ( "strconv" "github.com/autonomy/talos/internal/pkg/blockdevice" + "github.com/autonomy/talos/internal/pkg/blockdevice/bootloader/syslinux" + "github.com/autonomy/talos/internal/pkg/blockdevice/filesystem/vfat" "github.com/autonomy/talos/internal/pkg/blockdevice/filesystem/xfs" "github.com/autonomy/talos/internal/pkg/blockdevice/probe" "github.com/autonomy/talos/internal/pkg/blockdevice/table" @@ -178,6 +180,11 @@ func Prepare(data *userdata.UserData) (err error) { continue } + if label == constants.BootPartitionLabel { + if err = syslinux.Prepare(device); err != nil { + return err + } + } var bd *blockdevice.BlockDevice bd, err = blockdevice.Open(device, blockdevice.WithNewGPT(data.Install.Wipe)) @@ -241,7 +248,7 @@ func Prepare(data *userdata.UserData) (err error) { for _, dev := range devices { // Create the filesystem - log.Printf("Formatting Partition %s - %s\n", dev.Name, dev.Label) + log.Printf("Formatting Partition %s - %s\n", dev.PartitionName, dev.Label) err = dev.Format() if err != nil { return err @@ -296,11 +303,15 @@ func NewDevice(name string, label string, size uint, force bool, test bool, data // Partition creates a new partition on the specified device // nolint: dupl func (d *Device) Partition() error { - var typeID string + var ( + typeID string + legacyBIOSBootable bool + ) switch d.Label { case constants.BootPartitionLabel: // EFI System Partition typeID = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + legacyBIOSBootable = true case constants.RootPartitionLabel: // Root Partition switch runtime.GOARCH { @@ -318,7 +329,13 @@ func (d *Device) Partition() error { return errors.Errorf("%s", "unknown partition label") } - part, err := d.PartitionTable.Add(uint64(d.Size), partition.WithPartitionType(typeID), partition.WithPartitionName(d.Label), partition.WithPartitionTest(d.Test)) + part, err := d.PartitionTable.Add( + uint64(d.Size), + partition.WithPartitionType(typeID), + partition.WithPartitionName(d.Label), + partition.WithLegacyBIOSBootableAttribute(legacyBIOSBootable), + partition.WithPartitionTest(d.Test), + ) if err != nil { return err } @@ -330,5 +347,8 @@ func (d *Device) Partition() error { // Format creates a xfs filesystem on the device/partition func (d *Device) Format() error { + if d.Label == constants.BootPartitionLabel { + return vfat.MakeFS(d.PartitionName, vfat.WithLabel(d.Label)) + } return xfs.MakeFS(d.PartitionName, xfs.WithLabel(d.Label), xfs.WithForce(d.Force)) } diff --git a/internal/pkg/kernel/kernel.go b/internal/pkg/kernel/kernel.go index b40c3db27..036730903 100644 --- a/internal/pkg/kernel/kernel.go +++ b/internal/pkg/kernel/kernel.go @@ -9,11 +9,21 @@ import ( "strings" ) +// ReadProcCmdline reads /proc/cmdline. +func ReadProcCmdline() (cmdlineBytes []byte, err error) { + cmdlineBytes, err = ioutil.ReadFile("/proc/cmdline") + if err != nil { + return nil, err + } + + return cmdlineBytes, nil +} + // ParseProcCmdline parses /proc/cmdline and returns a map reprentation of the // kernel parameters. func ParseProcCmdline() (cmdline map[string]string, err error) { var cmdlineBytes []byte - cmdlineBytes, err = ioutil.ReadFile("/proc/cmdline") + cmdlineBytes, err = ReadProcCmdline() if err != nil { return }