| // Copyright 2020 The Monogon Project Authors. |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // This package provides self-contained implementation used to generate |
| // Metropolis disk images. |
| package osimage |
| |
| import ( |
| "fmt" |
| "io" |
| "strings" |
| |
| "github.com/google/uuid" |
| |
| "source.monogon.dev/metropolis/pkg/blockdev" |
| "source.monogon.dev/metropolis/pkg/efivarfs" |
| "source.monogon.dev/metropolis/pkg/fat32" |
| "source.monogon.dev/metropolis/pkg/gpt" |
| ) |
| |
| var ( |
| SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c") |
| SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c") |
| |
| DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966") |
| ) |
| |
| const ( |
| SystemALabel = "METROPOLIS-SYSTEM-A" |
| SystemBLabel = "METROPOLIS-SYSTEM-B" |
| DataLabel = "METROPOLIS-NODE-DATA" |
| ESPLabel = "ESP" |
| |
| EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI" |
| EFIBootAPath = "/EFI/metropolis/boot-a.efi" |
| EFIBootBPath = "/EFI/metropolis/boot-b.efi" |
| nodeParamsPath = "metropolis/parameters.pb" |
| ) |
| |
| // PartitionSizeInfo contains parameters used during partition table |
| // initialization and, in case of image files, space allocation. |
| type PartitionSizeInfo struct { |
| // Size of the EFI System Partition (ESP), in mebibytes. The size must |
| // not be zero. |
| ESP int64 |
| // Size of the Metropolis system partition, in mebibytes. The partition |
| // won't be created if the size is zero. |
| System int64 |
| // Size of the Metropolis data partition, in mebibytes. The partition |
| // won't be created if the size is zero. If the image is output to a |
| // block device, the partition will be extended to fill the remaining |
| // space. |
| Data int64 |
| } |
| |
| // Params contains parameters used by Create to build a Metropolis OS |
| // image. |
| type Params struct { |
| // Output is the block device to which the OS image is written. |
| Output blockdev.BlockDev |
| // ABLoader provides the A/B loader which then loads the EFI loader for the |
| // correct slot. |
| ABLoader fat32.SizedReader |
| // EFIPayload provides contents of the EFI payload file. It must not be |
| // nil. This gets put into boot slot A. |
| EFIPayload fat32.SizedReader |
| // SystemImage provides contents of the Metropolis system partition. |
| // If nil, no contents will be copied into the partition. |
| SystemImage io.Reader |
| // NodeParameters provides contents of the node parameters file. If nil, |
| // the node parameters file won't be created in the target ESP |
| // filesystem. |
| NodeParameters fat32.SizedReader |
| // DiskGUID is a unique identifier of the image and a part of Table |
| // header. It's optional and can be left blank if the identifier is |
| // to be randomly generated. Setting it to a predetermined value can |
| // help in implementing reproducible builds. |
| DiskGUID uuid.UUID |
| // PartitionSize specifies a size for the ESP, Metropolis System and |
| // Metropolis data partition. |
| PartitionSize PartitionSizeInfo |
| } |
| |
| const Mi = 1024 * 1024 |
| |
| // Create writes a Metropolis OS image to a block device. |
| func Create(params *Params) (*efivarfs.LoadOption, error) { |
| // Discard the entire device, we're going to write new data over it. |
| // Ignore errors, this is only advisory. |
| params.Output.Discard(0, params.Output.BlockCount()) |
| |
| tbl, err := gpt.New(params.Output) |
| if err != nil { |
| return nil, fmt.Errorf("invalid block device: %w", err) |
| } |
| tbl.ID = params.DiskGUID |
| esp := gpt.Partition{ |
| Type: gpt.PartitionTypeEFISystem, |
| Name: ESPLabel, |
| } |
| if err := tbl.AddPartition(&esp, params.PartitionSize.ESP*Mi); err != nil { |
| return nil, fmt.Errorf("failed to allocate ESP: %w", err) |
| } |
| |
| rootInode := fat32.Inode{ |
| Attrs: fat32.AttrDirectory, |
| } |
| if err := rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), params.EFIPayload); err != nil { |
| return nil, err |
| } |
| // Place the A/B loader at the EFI bootloader autodiscovery path. |
| if err := rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.ABLoader); err != nil { |
| return nil, err |
| } |
| if params.NodeParameters != nil { |
| if err := rootInode.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil { |
| return nil, err |
| } |
| } |
| if err := fat32.WriteFS(blockdev.NewRWS(esp), rootInode, fat32.Options{ |
| BlockSize: uint16(esp.BlockSize()), |
| BlockCount: uint32(esp.BlockCount()), |
| Label: "MNGN_BOOT", |
| }); err != nil { |
| return nil, fmt.Errorf("failed to write FAT32: %w", err) |
| } |
| |
| // Create the system partition only if its size is specified. |
| if params.PartitionSize.System != 0 && params.SystemImage != nil { |
| systemPartitionA := gpt.Partition{ |
| Type: SystemAType, |
| Name: SystemALabel, |
| } |
| if err := tbl.AddPartition(&systemPartitionA, params.PartitionSize.System*Mi); err != nil { |
| return nil, fmt.Errorf("failed to allocate system partition A: %w", err) |
| } |
| if _, err := io.Copy(blockdev.NewRWS(systemPartitionA), params.SystemImage); err != nil { |
| return nil, fmt.Errorf("failed to write system partition A: %w", err) |
| } |
| systemPartitionB := gpt.Partition{ |
| Type: SystemBType, |
| Name: SystemBLabel, |
| } |
| if err := tbl.AddPartition(&systemPartitionB, params.PartitionSize.System*Mi); err != nil { |
| return nil, fmt.Errorf("failed to allocate system partition B: %w", err) |
| } |
| } else if params.PartitionSize.System == 0 && params.SystemImage != nil { |
| // Safeguard against contradicting parameters. |
| return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero") |
| } |
| // Create the data partition only if its size is specified. |
| if params.PartitionSize.Data != 0 { |
| dataPartition := gpt.Partition{ |
| Type: DataType, |
| Name: DataLabel, |
| } |
| if err := tbl.AddPartition(&dataPartition, -1); err != nil { |
| return nil, fmt.Errorf("failed to allocate data partition: %w", err) |
| } |
| } |
| |
| if err := tbl.Write(); err != nil { |
| return nil, fmt.Errorf("failed to write Table: %w", err) |
| } |
| |
| // Build an EFI boot entry pointing to the image's ESP. |
| return &efivarfs.LoadOption{ |
| Description: "Metropolis", |
| FilePath: efivarfs.DevicePath{ |
| &efivarfs.HardDrivePath{ |
| PartitionNumber: 1, |
| PartitionStartBlock: esp.FirstBlock, |
| PartitionSizeBlocks: esp.SizeBlocks(), |
| PartitionMatch: efivarfs.PartitionGPT{ |
| PartitionUUID: esp.ID, |
| }, |
| }, |
| efivarfs.FilePath(EFIPayloadPath), |
| }, |
| }, nil |
| } |