treewide: port everything to blockdev
This gets rid of most ad-hoc block device code, using blockdev for
everything. It also gets rid of diskfs for everything but tests. This
enables Metropolis to be installed on non-512-byte block sizes.
Change-Id: I644b5b68bb7bed8106585df3179674789031687a
Reviewed-on: https://review.monogon.dev/c/monogon/+/1873
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index f013c42..111c6e6 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -46,6 +46,8 @@
"//metropolis/cli/pkg/datafile",
"//metropolis/node/core/identity",
"//metropolis/node/core/rpc",
+ "//metropolis/pkg/blkio",
+ "//metropolis/pkg/fat32",
"//metropolis/pkg/logtree",
"//metropolis/proto/api",
"//metropolis/proto/common",
diff --git a/metropolis/cli/metroctl/cmd_install.go b/metropolis/cli/metroctl/cmd_install.go
index 9ad7953..39790f1 100644
--- a/metropolis/cli/metroctl/cmd_install.go
+++ b/metropolis/cli/metroctl/cmd_install.go
@@ -15,6 +15,8 @@
"source.monogon.dev/metropolis/cli/metroctl/core"
clicontext "source.monogon.dev/metropolis/cli/pkg/context"
"source.monogon.dev/metropolis/cli/pkg/datafile"
+ "source.monogon.dev/metropolis/pkg/blkio"
+ "source.monogon.dev/metropolis/pkg/fat32"
"source.monogon.dev/metropolis/proto/api"
cpb "source.monogon.dev/metropolis/proto/common"
)
@@ -49,30 +51,21 @@
size uint64
}
-func external(name, datafilePath string, flag *string) *externalFile {
+func external(name, datafilePath string, flag *string) fat32.SizedReader {
if flag == nil || *flag == "" {
df, err := datafile.Get(datafilePath)
if err != nil {
log.Fatalf("No %s specified", name)
}
- return &externalFile{
- reader: bytes.NewReader(df),
- size: uint64(len(df)),
- }
+ return bytes.NewReader(df)
}
- f, err := os.Open(*bundlePath)
+ f, err := blkio.NewFileReader(*bundlePath)
if err != nil {
log.Fatalf("Failed to open specified %s: %v", name, err)
}
- st, err := f.Stat()
- if err != nil {
- log.Fatalf("Failed to stat specified %s: %v", name, err)
- }
- return &externalFile{
- reader: f,
- size: uint64(st.Size()),
- }
+
+ return f
}
func doGenUSB(cmd *cobra.Command, args []string) {
@@ -154,12 +147,10 @@
}
installerImageArgs := core.MakeInstallerImageArgs{
- TargetPath: args[0],
- Installer: installer.reader,
- InstallerSize: installer.size,
- NodeParams: params,
- Bundle: bundle.reader,
- BundleSize: bundle.size,
+ TargetPath: args[0],
+ Installer: installer,
+ NodeParams: params,
+ Bundle: bundle,
}
log.Printf("Generating installer image (this can take a while, see issues/92).")
diff --git a/metropolis/cli/metroctl/core/BUILD.bazel b/metropolis/cli/metroctl/core/BUILD.bazel
index 133ff2e..7c1a0f4 100644
--- a/metropolis/cli/metroctl/core/BUILD.bazel
+++ b/metropolis/cli/metroctl/core/BUILD.bazel
@@ -14,11 +14,10 @@
"//metropolis/node",
"//metropolis/node/core/rpc",
"//metropolis/node/core/rpc/resolver",
+ "//metropolis/pkg/blockdev",
+ "//metropolis/pkg/fat32",
+ "//metropolis/pkg/gpt",
"//metropolis/proto/api",
- "@com_github_diskfs_go_diskfs//:go-diskfs",
- "@com_github_diskfs_go_diskfs//disk",
- "@com_github_diskfs_go_diskfs//filesystem",
- "@com_github_diskfs_go_diskfs//partition/gpt",
"@io_k8s_client_go//pkg/apis/clientauthentication/v1:clientauthentication",
"@io_k8s_client_go//tools/clientcmd",
"@io_k8s_client_go//tools/clientcmd/api",
diff --git a/metropolis/cli/metroctl/core/install.go b/metropolis/cli/metroctl/core/install.go
index c20e38d..31b7328 100644
--- a/metropolis/cli/metroctl/core/install.go
+++ b/metropolis/cli/metroctl/core/install.go
@@ -1,168 +1,100 @@
package core
import (
+ "bytes"
"errors"
"fmt"
- "io"
+ "math"
"os"
- "github.com/diskfs/go-diskfs"
- "github.com/diskfs/go-diskfs/disk"
- "github.com/diskfs/go-diskfs/filesystem"
- "github.com/diskfs/go-diskfs/partition/gpt"
"google.golang.org/protobuf/proto"
+ "source.monogon.dev/metropolis/pkg/blockdev"
+ "source.monogon.dev/metropolis/pkg/fat32"
+ "source.monogon.dev/metropolis/pkg/gpt"
"source.monogon.dev/metropolis/proto/api"
)
-func mibToSectors(size uint64, logicalBlockSize int64) uint64 {
- return (size * 1024 * 1024) / uint64(logicalBlockSize)
-}
-
-// Mask for aligning values to 1MiB boundaries. Go complains if you shift
-// 1 bits out of the value in a constant so the construction is a bit
-// convoluted.
-const align1MiBMask = (1<<44 - 1) << 20
-
-const MiB = 1024 * 1024
-
type MakeInstallerImageArgs struct {
// Path to either a file or a disk which will contain the installer data.
TargetPath string
// Reader for the installer EFI executable. Mandatory.
- Installer io.Reader
- InstallerSize uint64
+ Installer fat32.SizedReader
// Optional NodeParameters to be embedded for use by the installer.
NodeParams *api.NodeParameters
// Optional Reader for a Metropolis bundle for use by the installer.
- Bundle io.Reader
- BundleSize uint64
+ Bundle fat32.SizedReader
}
-// MakeInstallerImage generates an installer disk image containing a GPT
+// MakeInstallerImage generates an installer disk image containing a Table
// partition table and a single FAT32 partition with an installer and optionally
// with a bundle and/or Node Parameters.
func MakeInstallerImage(args MakeInstallerImageArgs) error {
if args.Installer == nil {
return errors.New("Installer is mandatory")
}
- if args.InstallerSize == 0 {
- return errors.New("InstallerSize needs to be valid (>0)")
- }
- if args.Bundle != nil && args.BundleSize == 0 {
- return errors.New("if a Bundle is passed BundleSize needs to be valid (>0)")
- }
- var err error
- var nodeParamsRaw []byte
- if args.NodeParams != nil {
- nodeParamsRaw, err = proto.Marshal(args.NodeParams)
- if err != nil {
- return fmt.Errorf("failed to marshal node params: %w", err)
- }
- }
+ espRoot := fat32.Inode{Attrs: fat32.AttrDirectory}
- var img *disk.Disk
- // The following section is a bit ugly, it would technically be nicer to
- // just pack all clusters of the FAT32 together, figure out how many were
- // needed at the end and truncate the partition there. But that would
- // require writing a new FAT32 writer, the effort to do that is in no way
- // proportional to its advantages. So let's just do some conservative
- // calculations on how much space we need and call it a day.
-
- // ~4MiB FAT32 headers, 1MiB alignment overhead (bitmask drops up to 1MiB),
- // 5% filesystem overhead
- partitionSizeBytes := (uint64(float32(5*MiB+args.BundleSize+args.InstallerSize+uint64(len(nodeParamsRaw))) * 1.05)) & align1MiBMask
- // FAT32 has a minimum partition size of 32MiB, so clamp the lower partition
- // size to a notch more than that.
- minimumSize := uint64(33 * MiB)
- if partitionSizeBytes < minimumSize {
- partitionSizeBytes = minimumSize
- }
- // If creating an image, create it with minimal size, i.e. 1MiB at each
- // end for partitioning metadata and alignment.
- // 1MiB alignment is used as that will essentially guarantee that any
- // partition is aligned to whatever internal block size is used by the
- // storage device. Especially flash-based storage likes to use much bigger
- // blocks than advertised as sectors which can degrade performance when
- // partitions are misaligned.
- calculatedImageBytes := 2*MiB + partitionSizeBytes
-
- if _, err = os.Stat(args.TargetPath); os.IsNotExist(err) {
- img, err = diskfs.Create(args.TargetPath, int64(calculatedImageBytes), diskfs.Raw)
- } else {
- img, err = diskfs.Open(args.TargetPath)
- }
- if err != nil {
- return fmt.Errorf("failed to create/open target: %w", err)
- }
- defer img.File.Close()
- // This has an edge case where the data would technically fit but our 5%
- // overhead are too conservative. But it is very rare and I don't really
- // trust diskfs to generate good errors when it overflows so we'll just
- // reject early.
- if uint64(img.Size) < calculatedImageBytes {
- return errors.New("target too small, data won't fit")
- }
-
- gptTable := &gpt.Table{
- LogicalSectorSize: int(img.LogicalBlocksize),
- PhysicalSectorSize: int(img.PhysicalBlocksize),
- ProtectiveMBR: true,
- Partitions: []*gpt.Partition{
- {
- Type: gpt.EFISystemPartition,
- Name: "MetropolisInstaller",
- Start: mibToSectors(1, img.LogicalBlocksize),
- Size: partitionSizeBytes,
- },
- },
- }
- if err := img.Partition(gptTable); err != nil {
- return fmt.Errorf("failed to partition target: %w", err)
- }
- fs, err := img.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "METRO_INST"})
- if err != nil {
- return fmt.Errorf("failed to create target filesystem: %w", err)
- }
-
- // Create EFI partition structure.
- for _, dir := range []string{"/EFI", "/EFI/BOOT", "/metropolis-installer"} {
- if err := fs.Mkdir(dir); err != nil {
- panic(err)
- }
- }
// This needs to be a "Removable Media" according to the UEFI Specification
// V2.9 Section 3.5.1.1. This file is booted by any compliant UEFI firmware
// in absence of another bootable boot entry.
- installerFile, err := fs.OpenFile("/EFI/BOOT/BOOTx64.EFI", os.O_CREATE|os.O_RDWR)
- if err != nil {
- panic(err)
+ if err := espRoot.PlaceFile("EFI/BOOT/BOOTx64.EFI", args.Installer); err != nil {
+ return err
}
- if _, err := io.Copy(installerFile, args.Installer); err != nil {
- return fmt.Errorf("failed to copy installer file: %w", err)
- }
+
if args.NodeParams != nil {
- nodeParamsFile, err := fs.OpenFile("/metropolis-installer/nodeparams.pb", os.O_CREATE|os.O_RDWR)
+ nodeParamsRaw, err := proto.Marshal(args.NodeParams)
if err != nil {
- panic(err)
+ return fmt.Errorf("failed to marshal node params: %w", err)
}
- _, err = nodeParamsFile.Write(nodeParamsRaw)
- if err != nil {
- return fmt.Errorf("failed to write node params: %w", err)
+ if err := espRoot.PlaceFile("metropolis-installer/nodeparams.pb", bytes.NewReader(nodeParamsRaw)); err != nil {
+ return err
}
}
if args.Bundle != nil {
- bundleFile, err := fs.OpenFile("/metropolis-installer/bundle.bin", os.O_CREATE|os.O_RDWR)
+ if err := espRoot.PlaceFile("metropolis-installer/bundle.bin", args.Bundle); err != nil {
+ return err
+ }
+ }
+ var targetDev blockdev.BlockDev
+ var err error
+ targetDev, err = blockdev.Open(args.TargetPath)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ targetDev, err = blockdev.CreateFile(args.TargetPath, 512, 1024*1024+4096)
+ }
if err != nil {
- panic(err)
+ return fmt.Errorf("unable to open target device: %w", err)
}
- if _, err := io.Copy(bundleFile, args.Bundle); err != nil {
- return fmt.Errorf("failed to copy bundle: %w", err)
- }
+ }
+ partTable, err := gpt.New(targetDev)
+ if err != nil {
+ return fmt.Errorf("target device has invalid geometry: %w", err)
+ }
+ esp := gpt.Partition{
+ Type: gpt.PartitionTypeEFISystem,
+ Name: "MetropolisInstaller",
+ }
+ fatOpts := fat32.Options{Label: "METRO_INST"}
+ // TODO(#254): Build and use dynamically-grown block devices
+ var espSize int64 = 512 * 1024 * 1024
+ if err := partTable.AddPartition(&esp, espSize); err != nil {
+ return fmt.Errorf("unable to create partition layout: %w", err)
+ }
+ if esp.BlockSize() > math.MaxUint16 {
+ return fmt.Errorf("block size (%d) too large for FAT32", esp.BlockSize())
+ }
+ fatOpts.BlockSize = uint16(esp.BlockSize())
+ fatOpts.BlockCount = uint32(esp.BlockCount())
+ if err := fat32.WriteFS(blockdev.NewRWS(esp), espRoot, fatOpts); err != nil {
+ return fmt.Errorf("failed to write FAT32: %w", err)
+ }
+ if err := partTable.Write(); err != nil {
+ return fmt.Errorf("unable to write partition table: %w", err)
}
return nil
}