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