blob: 35b49eac791ceac2dd18dda0910f381d80dc8d53 [file] [log] [blame]
Lorenz Brun57479bb2021-10-26 14:01:06 +02001package core
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "os"
8
9 "github.com/diskfs/go-diskfs"
10 "github.com/diskfs/go-diskfs/disk"
11 "github.com/diskfs/go-diskfs/filesystem"
12 "github.com/diskfs/go-diskfs/partition/gpt"
13 "google.golang.org/protobuf/proto"
14 "source.monogon.dev/metropolis/proto/api"
15)
16
17func mibToSectors(size uint64, logicalBlockSize int64) uint64 {
18 return (size * 1024 * 1024) / uint64(logicalBlockSize)
19}
20
21// Mask for aligning values to 1MiB boundaries. Go complains if you shift
22// 1 bits out of the value in a constant so the construction is a bit
23// convoluted.
24const align1MiBMask = (1<<44 - 1) << 20
25
26const MiB = 1024 * 1024
27
28type MakeInstallerImageArgs struct {
29 // Path to either a file or a disk which will contain the installer data.
30 TargetPath string
31
32 // Reader for the installer EFI executable. Mandatory.
33 Installer io.Reader
34 InstallerSize uint64
35
36 // Optional NodeParameters to be embedded for use by the installer.
37 NodeParams *api.NodeParameters
38
39 // Optional Reader for a Metropolis bundle for use by the installer.
40 Bundle io.Reader
41 BundleSize uint64
42}
43
44// MakeInstallerImage generates an installer disk image containing a GPT
45// partition table and a single FAT32 partition with an installer and optionally
46// with a bundle and/or Node Parameters.
47func MakeInstallerImage(args MakeInstallerImageArgs) error {
48 if args.Installer == nil {
49 return errors.New("Installer is mandatory")
50 }
51 if args.InstallerSize == 0 {
52 return errors.New("InstallerSize needs to be valid (>0)")
53 }
54 if args.Bundle != nil && args.BundleSize == 0 {
55 return errors.New("if a Bundle is passed BundleSize needs to be valid (>0)")
56 }
57
58 var err error
59 var nodeParamsRaw []byte
60 if args.NodeParams != nil {
61 nodeParamsRaw, err = proto.Marshal(args.NodeParams)
62 if err != nil {
63 return fmt.Errorf("failed to marshal node params: %w", err)
64 }
65 }
66
67 var img *disk.Disk
68 // The following section is a bit ugly, it would technically be nicer to
69 // just pack all clusters of the FAT32 together, figure out how many were
70 // needed at the end and truncate the partition there. But that would
71 // require writing a new FAT32 writer, the effort to do that is in no way
72 // proportional to its advantages. So let's just do some conservative
73 // calculations on how much space we need and call it a day.
74
75 // ~4MiB FAT32 headers, 1MiB alignment overhead (bitmask drops up to 1MiB),
76 // 5% filesystem overhead
77 partitionSizeBytes := (uint64(float32(5*MiB+args.BundleSize+args.InstallerSize+uint64(len(nodeParamsRaw))) * 1.05)) & align1MiBMask
78 // FAT32 has a minimum partition size of 32MiB, so clamp the lower partition
79 // size to that.
80 if partitionSizeBytes < 32*MiB {
81 partitionSizeBytes = 32 * MiB
82 }
83 // If creating an image, create it with minimal size, i.e. 1MiB at each
84 // end for partitioning metadata and alignment.
85 // 1MiB alignment is used as that will essentially guarantee that any
86 // partition is aligned to whatever internal block size is used by the
87 // storage device. Especially flash-based storage likes to use much bigger
88 // blocks than advertised as sectors which can degrade performance when
89 // partitions are misaligned.
90 calculatedImageBytes := 2*MiB + partitionSizeBytes
91
92 if _, err = os.Stat(args.TargetPath); os.IsNotExist(err) {
93 img, err = diskfs.Create(args.TargetPath, int64(calculatedImageBytes), diskfs.Raw)
94 } else {
95 img, err = diskfs.Open(args.TargetPath)
96 }
97 if err != nil {
98 return fmt.Errorf("failed to create/open target: %w", err)
99 }
100 defer img.File.Close()
101 // This has an edge case where the data would technically fit but our 5%
102 // overhead are too conservative. But it is very rare and I don't really
103 // trust diskfs to generate good errors when it overflows so we'll just
104 // reject early.
105 if uint64(img.Size) < calculatedImageBytes {
106 return errors.New("target too small, data won't fit")
107 }
108
109 gptTable := &gpt.Table{
110 LogicalSectorSize: int(img.LogicalBlocksize),
111 PhysicalSectorSize: int(img.PhysicalBlocksize),
112 ProtectiveMBR: true,
113 Partitions: []*gpt.Partition{
114 {
115 Type: gpt.EFISystemPartition,
116 Name: "MetropolisInstaller",
117 Start: mibToSectors(1, img.LogicalBlocksize),
118 Size: partitionSizeBytes,
119 },
120 },
121 }
122 if err := img.Partition(gptTable); err != nil {
123 return fmt.Errorf("failed to partition target: %w", err)
124 }
125 fs, err := img.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "METRO_INST"})
126 if err != nil {
127 return fmt.Errorf("failed to create target filesystem: %w", err)
128 }
129
130 // Create EFI partition structure.
131 for _, dir := range []string{"/EFI", "/EFI/BOOT", "/EFI/metropolis-installer"} {
132 if err := fs.Mkdir(dir); err != nil {
133 panic(err)
134 }
135 }
136 // This needs to be a "Removable Media" according to the UEFI Specification
137 // V2.9 Section 3.5.1.1. This file is booted by any compliant UEFI firmware
138 // in absence of another bootable boot entry.
139 installerFile, err := fs.OpenFile("/EFI/BOOT/BOOTx64.EFI", os.O_CREATE|os.O_RDWR)
140 if err != nil {
141 panic(err)
142 }
143 if _, err := io.Copy(installerFile, args.Installer); err != nil {
144 return fmt.Errorf("failed to copy installer file: %w", err)
145 }
146 if args.NodeParams != nil {
147 nodeParamsFile, err := fs.OpenFile("/EFI/metropolis-installer/nodeparams.pb", os.O_CREATE|os.O_RDWR)
148 if err != nil {
149 panic(err)
150 }
151 _, err = nodeParamsFile.Write(nodeParamsRaw)
152 if err != nil {
153 return fmt.Errorf("failed to write node params: %w", err)
154 }
155 }
156 if args.Bundle != nil {
157 bundleFile, err := fs.OpenFile("/EFI/metropolis-installer/bundle.bin", os.O_CREATE|os.O_RDWR)
158 if err != nil {
159 panic(err)
160 }
161 if _, err := io.Copy(bundleFile, args.Bundle); err != nil {
162 return fmt.Errorf("failed to copy bundle: %w", err)
163 }
164 }
165 return nil
166}