blob: 2f000a6b86d38a357454395bf56649f594a8a18d [file] [log] [blame]
Mateusz Zalegac71efc92021-09-07 16:46:25 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// This package provides self-contained implementation used to generate
18// Metropolis disk images.
19package osimage
20
21import (
22 "fmt"
23 "io"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020024 "os"
25
26 diskfs "github.com/diskfs/go-diskfs"
27 "github.com/diskfs/go-diskfs/disk"
28 "github.com/diskfs/go-diskfs/filesystem"
29 "github.com/diskfs/go-diskfs/partition/gpt"
30
31 "source.monogon.dev/metropolis/pkg/efivarfs"
32)
33
34const (
35 systemPartitionType = gpt.Type("ee96055b-f6d0-4267-8bbb-724b2afea74c")
36 SystemVolumeLabel = "METROPOLIS-SYSTEM"
37
38 dataPartitionType = gpt.Type("9eeec464-6885-414a-b278-4305c51f7966")
39 DataVolumeLabel = "METROPOLIS-NODE-DATA"
40
41 ESPVolumeLabel = "ESP"
42
43 EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
44 nodeParamsPath = "/EFI/metropolis/parameters.pb"
45
46 mib = 1024 * 1024
47)
48
49// put creates a file on the target filesystem fs and fills it with
50// contents read from an io.Reader object src.
51func put(fs filesystem.FileSystem, dst string, src io.Reader) error {
52 target, err := fs.OpenFile(dst, os.O_CREATE|os.O_RDWR)
53 if err != nil {
54 return fmt.Errorf("while opening %q: %w", dst, err)
55 }
56
57 // If this is streamed (e.g. using io.Copy) it exposes a bug in diskfs, so
58 // do it in one go.
59 // TODO(mateusz@monogon.tech): Investigate the bug.
Lorenz Brun764a2de2021-11-22 16:26:36 +010060 data, err := io.ReadAll(src)
Mateusz Zalegac71efc92021-09-07 16:46:25 +020061 if err != nil {
62 return fmt.Errorf("while reading %q: %w", src, err)
63 }
64 if _, err := target.Write(data); err != nil {
65 return fmt.Errorf("while writing to %q: %w", dst, err)
66 }
67 return nil
68}
69
70// initializeESP creates an ESP filesystem in a partition specified by
71// index. It then creates the EFI executable and copies into it contents
72// of the reader object exec, which must not be nil. The node parameters
73// file is optionally created if params is not nil. initializeESP may return
74// an error.
75func initializeESP(image *disk.Disk, index int, exec, params io.Reader) error {
76 // Create a FAT ESP filesystem inside a partition pointed to by
77 // index.
78 spec := disk.FilesystemSpec{
79 Partition: index,
80 FSType: filesystem.TypeFat32,
81 VolumeLabel: ESPVolumeLabel,
82 }
83 fs, err := image.CreateFilesystem(spec)
84 if err != nil {
85 return fmt.Errorf("while creating an ESP filesystem: %w", err)
86 }
87
88 // Create the EFI partition structure.
89 for _, dir := range []string{"/EFI", "/EFI/BOOT", "/EFI/metropolis"} {
90 if err := fs.Mkdir(dir); err != nil {
91 return fmt.Errorf("while creating %q: %w", dir, err)
92 }
93 }
94
95 // Copy the EFI payload to the newly created filesystem.
96 if exec == nil {
97 return fmt.Errorf("exec must not be nil")
98 }
99 if err := put(fs, EFIPayloadPath, exec); err != nil {
100 return fmt.Errorf("while writing an EFI payload: %w", err)
101 }
102
103 if params != nil {
104 // Copy Node Parameters into the ESP.
105 if err := put(fs, nodeParamsPath, params); err != nil {
106 return fmt.Errorf("while writing node parameters: %w", err)
107 }
108 }
109 return nil
110}
111
112// zeroSrc is a source of null bytes implementing io.Reader. It acts as a
113// helper data type.
114type zeroSrc struct{}
115
116// Read implements io.Reader for zeroSrc. It fills b with zero bytes. The
117// returned error is always nil.
118func (_ zeroSrc) Read(b []byte) (n int, err error) {
119 for i := range b {
120 b[i] = 0
121 }
122 return len(b), nil
123}
124
125// initializeSystemPartition copies system partition contents into a partition
126// at index. The remaining partition space is zero-padded. This function may
127// return an error.
128func initializeSystemPartition(image *disk.Disk, index int, contents io.Reader) error {
129 // Check the parameters.
130 if contents == nil {
131 return fmt.Errorf("system partition contents parameter must not be nil")
132 }
133 if index <= 0 {
134 return fmt.Errorf("partition index must be greater than zero")
135 }
136
137 // Get the system partition's size.
138 table, err := image.GetPartitionTable()
139 if err != nil {
140 return fmt.Errorf("while accessing a go-diskfs partition table: %w", err)
141 }
142 partitions := table.GetPartitions()
143 if index > len(partitions) {
144 return fmt.Errorf("partition index out of bounds")
145 }
146 size := partitions[index-1].GetSize()
147
148 // Copy the contents of the Metropolis system image into the system partition
149 // at partitionIndex. Zero-pad the remaining space.
150 var zero zeroSrc
151 sys := io.LimitReader(io.MultiReader(contents, zero), size)
152 if _, err := image.WritePartitionContents(index, sys); err != nil {
153 return fmt.Errorf("while copying the system partition: %w", err)
154 }
155 return nil
156}
157
158// mibToSectors converts a size expressed in mebibytes to a number of
159// sectors needed to store data of that size. sectorSize parameter
160// specifies the size of a logical sector.
161func mibToSectors(size, sectorSize uint64) uint64 {
162 return (size * mib) / sectorSize
163}
164
165// PartitionSizeInfo contains parameters used during partition table
166// initialization and, in case of image files, space allocation.
167type PartitionSizeInfo struct {
168 // Size of the EFI System Partition (ESP), in mebibytes. The size must
169 // not be zero.
170 ESP uint64
171 // Size of the Metropolis system partition, in mebibytes. The partition
172 // won't be created if the size is zero.
173 System uint64
174 // Size of the Metropolis data partition, in mebibytes. The partition
175 // won't be created if the size is zero. If the image is output to a
176 // block device, the partition will be extended to fill the remaining
177 // space.
178 Data uint64
179}
180
181// partitionList stores partition definitions in an ascending order.
182type partitionList []*gpt.Partition
183
184// appendPartition puts a new partition at the end of a partitionList,
185// automatically calculating its start and end markers based on data in
186// the list and the argument psize. A partition type and a name are
187// assigned to the partition. The containing image is used to calculate
188// sector addresses based on its logical block size.
189func (pl *partitionList) appendPartition(image *disk.Disk, ptype gpt.Type, pname string, psize uint64) {
190 // Calculate the start and end marker.
191 var pstart, pend uint64
192 if len(*pl) != 0 {
193 pstart = (*pl)[len(*pl)-1].End + 1
194 } else {
195 pstart = mibToSectors(1, uint64(image.LogicalBlocksize))
196 }
197 pend = pstart + mibToSectors(psize, uint64(image.LogicalBlocksize)) - 1
198
199 // Put the new partition at the end of the list.
200 *pl = append(*pl, &gpt.Partition{
201 Type: ptype,
202 Name: pname,
203 Start: pstart,
204 End: pend,
205 })
206}
207
208// extendLastPartition moves the end marker of the last partition in a
209// partitionList to the end of image, assigning all of the remaining free
210// space to it. It may return an error.
211func (pl *partitionList) extendLastPartition(image *disk.Disk) error {
212 if len(*pl) == 0 {
213 return fmt.Errorf("no partitions defined")
214 }
215 if image.Size == 0 {
216 return fmt.Errorf("the image size mustn't be zero")
217 }
218 if image.LogicalBlocksize == 0 {
219 return fmt.Errorf("the image's logical block size mustn't be zero")
220 }
221
222 // The last 33 blocks are occupied by the Secondary GPT.
223 (*pl)[len(*pl)-1].End = uint64(image.Size/image.LogicalBlocksize) - 33
224 return nil
225}
226
227// initializePartitionTable applies a Metropolis-compatible GPT partition
228// table to an image. Logical and physical sector sizes are based on
229// block sizes exposed by Disk. Partition extents are defined by the size
230// argument. A diskGUID is associated with the partition table. In an event
231// the table couldn't have been applied, the function will return an error.
232func initializePartitionTable(image *disk.Disk, size *PartitionSizeInfo, diskGUID string) error {
233 // Start with preparing a partition list.
234 var pl partitionList
235 // Create the ESP.
236 if size.ESP == 0 {
237 return fmt.Errorf("ESP size mustn't be zero")
238 }
239 pl.appendPartition(image, gpt.EFISystemPartition, ESPVolumeLabel, size.ESP)
240 // Create the system partition only if its size is specified.
241 if size.System != 0 {
242 pl.appendPartition(image, systemPartitionType, SystemVolumeLabel, size.System)
243 }
244 // Create the data partition only if its size is specified.
245 if size.Data != 0 {
246 // Don't specify the last partition's length, as it will be extended
247 // to fit the image size anyway. size.Data will still be used in the
248 // space allocation step.
249 pl.appendPartition(image, dataPartitionType, DataVolumeLabel, 0)
250 if err := pl.extendLastPartition(image); err != nil {
251 return fmt.Errorf("while extending the last partition: %w", err)
252 }
253 }
254
255 // Build and apply the partition table.
256 table := &gpt.Table{
257 LogicalSectorSize: int(image.LogicalBlocksize),
258 PhysicalSectorSize: int(image.PhysicalBlocksize),
259 ProtectiveMBR: true,
260 GUID: diskGUID,
261 Partitions: pl,
262 }
263 if err := image.Partition(table); err != nil {
264 // Return the error unwrapped.
265 return err
266 }
267 return nil
268}
269
270// Params contains parameters used by Create to build a Metropolis OS
271// image.
272type Params struct {
273 // OutputPath is the path an OS image will be written to. If the path
274 // points to an existing block device, the data partition will be
275 // extended to fill it entirely. Otherwise, a regular image file will
276 // be created at OutputPath. The path must not point to an existing
277 // regular file.
278 OutputPath string
279 // EFIPayload provides contents of the EFI payload file. It must not be
280 // nil.
281 EFIPayload io.Reader
282 // SystemImage provides contents of the Metropolis system partition.
283 // If nil, no contents will be copied into the partition.
284 SystemImage io.Reader
285 // NodeParameters provides contents of the node parameters file. If nil,
286 // the node parameters file won't be created in the target ESP
287 // filesystem.
288 NodeParameters io.Reader
289 // DiskGUID is a unique identifier of the image and a part of GPT
290 // header. It's optional and can be left blank if the identifier is
291 // to be randomly generated. Setting it to a predetermined value can
292 // help in implementing reproducible builds.
293 DiskGUID string
294 // PartitionSize specifies a size for the ESP, Metropolis System and
295 // Metropolis data partition.
296 PartitionSize PartitionSizeInfo
297}
298
299// Create writes a Metropolis OS image to either a newly created regular
300// file or a block device. The image is parametrized by the params
301// argument. In case a regular file already exists at params.OutputPath,
302// the function will fail. It returns nil on success or an error, if one
303// did occur.
304func Create(params *Params) (*efivarfs.BootEntry, error) {
305 // Validate each parameter before use.
306 if params.OutputPath == "" {
307 return nil, fmt.Errorf("image output path must be set")
308 }
309
310 // Learn whether we're creating a new image or writing to an existing
311 // block device by stat-ing the output path parameter.
312 outInfo, err := os.Stat(params.OutputPath)
313 if err != nil && !os.IsNotExist(err) {
314 return nil, err
315 }
316
317 // Calculate the image size (bytes) by summing up partition sizes
318 // (mebibytes).
319 minSize := (int64(params.PartitionSize.ESP) +
320 int64(params.PartitionSize.System) +
321 int64(params.PartitionSize.Data) + 1) * mib
322 var diskImg *disk.Disk
323 if !os.IsNotExist(err) && outInfo.Mode()&os.ModeDevice != 0 {
324 // Open the block device. The data partition size parameter won't
325 // matter in this case, as said partition will be extended till the
326 // end of device.
327 diskImg, err = diskfs.Open(params.OutputPath)
328 if err != nil {
329 return nil, fmt.Errorf("couldn't open the block device at %q: %w", params.OutputPath, err)
330 }
331 // Make sure there's at least minSize space available on the block
332 // device.
333 if minSize > diskImg.Size {
334 return nil, fmt.Errorf("not enough space available on the block device at %q", params.OutputPath)
335 }
336 } else {
337 // Attempt to create an image file at params.OutputPath. diskfs.Create
338 // will abort in case a file already exists at the given path.
339 // Calculate the image size expressed in bytes by summing up declared
340 // partition lengths.
341 diskImg, err = diskfs.Create(params.OutputPath, minSize, diskfs.Raw)
342 if err != nil {
343 return nil, fmt.Errorf("couldn't create a disk image at %q: %w", params.OutputPath, err)
344 }
345 }
346
347 // Go through the initialization steps, starting with applying a
348 // partition table according to params.
349 if err := initializePartitionTable(diskImg, &params.PartitionSize, params.DiskGUID); err != nil {
350 return nil, fmt.Errorf("failed to initialize the partition table: %w", err)
351 }
352 // The system partition will be created only if its specified size isn't
353 // equal to zero, making the initialization step optional as well. In
354 // addition, params.SystemImage must be set.
355 if params.PartitionSize.System != 0 && params.SystemImage != nil {
356 if err := initializeSystemPartition(diskImg, 2, params.SystemImage); err != nil {
357 return nil, fmt.Errorf("failed to initialize the system partition: %w", err)
358 }
359 } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
360 // Safeguard against contradicting parameters.
361 return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
362 }
363 // Attempt to initialize the ESP unconditionally, as it's the only
364 // partition guaranteed to be created regardless of params.PartitionSize.
365 if err := initializeESP(diskImg, 1, params.EFIPayload, params.NodeParameters); err != nil {
366 return nil, fmt.Errorf("failed to initialize the ESP: %w", err)
367 }
368 // The data partition, even if created, is always left uninitialized.
369
370 // Build an EFI boot entry pointing to the image's ESP. go-diskfs won't let
371 // you do that after you close the image.
372 t, err := diskImg.GetPartitionTable()
373 p := t.GetPartitions()
374 esp := (p[0]).(*gpt.Partition)
375 be := efivarfs.BootEntry{
376 Description: "Metropolis",
377 Path: EFIPayloadPath,
378 PartitionGUID: esp.GUID,
379 PartitionNumber: 1,
380 PartitionStart: esp.Start,
381 PartitionSize: esp.End - esp.Start + 1,
382 }
383 // Close the image and return the EFI boot entry.
384 if err := diskImg.File.Close(); err != nil {
385 return nil, fmt.Errorf("failed to finalize image: %w", err)
386 }
387 return &be, nil
388}