blob: 4cd3952b2713cb6538f2f7a79340d0c1e9987bb8 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Mateusz Zalegac71efc92021-09-07 16:46:25 +02002// SPDX-License-Identifier: Apache-2.0
Mateusz Zalegac71efc92021-09-07 16:46:25 +02003
4// This package provides self-contained implementation used to generate
5// Metropolis disk images.
6package osimage
7
8import (
9 "fmt"
10 "io"
Lorenz Brunad131882023-06-28 16:42:20 +020011 "strings"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020012
Mateusz Zalega612a0332021-11-17 20:04:52 +010013 "github.com/google/uuid"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020014
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020015 "source.monogon.dev/osbase/blockdev"
16 "source.monogon.dev/osbase/efivarfs"
17 "source.monogon.dev/osbase/fat32"
18 "source.monogon.dev/osbase/gpt"
Jan Schärc1b6df42025-03-20 08:52:18 +000019 "source.monogon.dev/osbase/structfs"
Lorenz Brunad131882023-06-28 16:42:20 +020020)
21
22var (
23 SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c")
24 SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c")
25
26 DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966")
Mateusz Zalegac71efc92021-09-07 16:46:25 +020027)
28
29const (
Lorenz Brun35fcf032023-06-29 04:15:58 +020030 SystemALabel = "METROPOLIS-SYSTEM-A"
31 SystemBLabel = "METROPOLIS-SYSTEM-B"
32 DataLabel = "METROPOLIS-NODE-DATA"
33 ESPLabel = "ESP"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020034
35 EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
Lorenz Brun35fcf032023-06-29 04:15:58 +020036 EFIBootAPath = "/EFI/metropolis/boot-a.efi"
37 EFIBootBPath = "/EFI/metropolis/boot-b.efi"
Lorenz Brunad131882023-06-28 16:42:20 +020038 nodeParamsPath = "metropolis/parameters.pb"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020039)
40
Mateusz Zalegac71efc92021-09-07 16:46:25 +020041// PartitionSizeInfo contains parameters used during partition table
42// initialization and, in case of image files, space allocation.
43type PartitionSizeInfo struct {
44 // Size of the EFI System Partition (ESP), in mebibytes. The size must
45 // not be zero.
Lorenz Brunad131882023-06-28 16:42:20 +020046 ESP int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020047 // Size of the Metropolis system partition, in mebibytes. The partition
48 // won't be created if the size is zero.
Lorenz Brunad131882023-06-28 16:42:20 +020049 System int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020050 // Size of the Metropolis data partition, in mebibytes. The partition
51 // won't be created if the size is zero. If the image is output to a
52 // block device, the partition will be extended to fill the remaining
53 // space.
Lorenz Brunad131882023-06-28 16:42:20 +020054 Data int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020055}
56
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020057// Params contains parameters used by Plan or Write to build a Metropolis OS
Mateusz Zalegac71efc92021-09-07 16:46:25 +020058// image.
59type Params struct {
Lorenz Brunad131882023-06-28 16:42:20 +020060 // Output is the block device to which the OS image is written.
61 Output blockdev.BlockDev
Lorenz Brun54a5a052023-10-02 16:40:11 +020062 // ABLoader provides the A/B loader which then loads the EFI loader for the
63 // correct slot.
Jan Schärc1b6df42025-03-20 08:52:18 +000064 ABLoader structfs.Blob
Mateusz Zalegac71efc92021-09-07 16:46:25 +020065 // EFIPayload provides contents of the EFI payload file. It must not be
Lorenz Brun54a5a052023-10-02 16:40:11 +020066 // nil. This gets put into boot slot A.
Jan Schärc1b6df42025-03-20 08:52:18 +000067 EFIPayload structfs.Blob
Mateusz Zalegac71efc92021-09-07 16:46:25 +020068 // SystemImage provides contents of the Metropolis system partition.
69 // If nil, no contents will be copied into the partition.
Jan Schärc1b6df42025-03-20 08:52:18 +000070 SystemImage structfs.Blob
Mateusz Zalegac71efc92021-09-07 16:46:25 +020071 // NodeParameters provides contents of the node parameters file. If nil,
72 // the node parameters file won't be created in the target ESP
73 // filesystem.
Jan Schärc1b6df42025-03-20 08:52:18 +000074 NodeParameters structfs.Blob
Lorenz Brunad131882023-06-28 16:42:20 +020075 // DiskGUID is a unique identifier of the image and a part of Table
Mateusz Zalegac71efc92021-09-07 16:46:25 +020076 // header. It's optional and can be left blank if the identifier is
77 // to be randomly generated. Setting it to a predetermined value can
78 // help in implementing reproducible builds.
Lorenz Brunad131882023-06-28 16:42:20 +020079 DiskGUID uuid.UUID
Mateusz Zalegac71efc92021-09-07 16:46:25 +020080 // PartitionSize specifies a size for the ESP, Metropolis System and
81 // Metropolis data partition.
82 PartitionSize PartitionSizeInfo
Tim Windelschmidt8e19fa42024-11-12 13:39:43 +000083 // BIOSBootCode provides the optional contents for the protective MBR
84 // block which gets executed by legacy BIOS boot.
85 BIOSBootCode []byte
Mateusz Zalegac71efc92021-09-07 16:46:25 +020086}
87
Tim Windelschmidtbceb1602024-07-10 18:17:32 +020088type plan struct {
89 *Params
Jan Schärc1b6df42025-03-20 08:52:18 +000090 efiRoot structfs.Tree
Tim Windelschmidtbceb1602024-07-10 18:17:32 +020091 tbl *gpt.Table
92 efiPartition *gpt.Partition
93 systemPartitionA *gpt.Partition
94 systemPartitionB *gpt.Partition
95 dataPartition *gpt.Partition
96}
Lorenz Brunad131882023-06-28 16:42:20 +020097
Tim Windelschmidtbceb1602024-07-10 18:17:32 +020098// Apply actually writes the planned installation to the blockdevice.
99func (i *plan) Apply() (*efivarfs.LoadOption, error) {
Lorenz Brunad131882023-06-28 16:42:20 +0200100 // Discard the entire device, we're going to write new data over it.
101 // Ignore errors, this is only advisory.
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200102 i.Output.Discard(0, i.Output.BlockCount()*i.Output.BlockSize())
Lorenz Brunad131882023-06-28 16:42:20 +0200103
Jan Schärc1b6df42025-03-20 08:52:18 +0000104 if err := fat32.WriteFS(blockdev.NewRWS(i.efiPartition), i.efiRoot, fat32.Options{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200105 BlockSize: uint16(i.efiPartition.BlockSize()),
106 BlockCount: uint32(i.efiPartition.BlockCount()),
Lorenz Brunad131882023-06-28 16:42:20 +0200107 Label: "MNGN_BOOT",
108 }); err != nil {
109 return nil, fmt.Errorf("failed to write FAT32: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200110 }
Lorenz Brunad131882023-06-28 16:42:20 +0200111
Jan Schärc1b6df42025-03-20 08:52:18 +0000112 systemImage, err := i.SystemImage.Open()
113 if err != nil {
114 return nil, fmt.Errorf("failed to open system image: %w", err)
115 }
116 if _, err := io.Copy(blockdev.NewRWS(i.systemPartitionA), systemImage); err != nil {
117 systemImage.Close()
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200118 return nil, fmt.Errorf("failed to write system partition A: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200119 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000120 systemImage.Close()
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200121
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200122 if err := i.tbl.Write(); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200123 return nil, fmt.Errorf("failed to write Table: %w", err)
Mateusz Zalega612a0332021-11-17 20:04:52 +0100124 }
Lorenz Brunad131882023-06-28 16:42:20 +0200125
126 // Build an EFI boot entry pointing to the image's ESP.
127 return &efivarfs.LoadOption{
Lorenz Brun54a5a052023-10-02 16:40:11 +0200128 Description: "Metropolis",
Lorenz Brunca1cff02023-06-26 17:52:44 +0200129 FilePath: efivarfs.DevicePath{
130 &efivarfs.HardDrivePath{
131 PartitionNumber: 1,
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200132 PartitionStartBlock: i.efiPartition.FirstBlock,
133 PartitionSizeBlocks: i.efiPartition.SizeBlocks(),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200134 PartitionMatch: efivarfs.PartitionGPT{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200135 PartitionUUID: i.efiPartition.ID,
Lorenz Brunca1cff02023-06-26 17:52:44 +0200136 },
137 },
Lorenz Brun54a5a052023-10-02 16:40:11 +0200138 efivarfs.FilePath(EFIPayloadPath),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200139 },
Lorenz Brunad131882023-06-28 16:42:20 +0200140 }, nil
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200141}
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200142
143// Plan allows to prepare an installation without modifying any data on the
144// system. To apply the planned installation, call Apply on the returned plan.
145func Plan(p *Params) (*plan, error) {
146 params := &plan{Params: p}
147
148 var err error
149 params.tbl, err = gpt.New(params.Output)
150 if err != nil {
151 return nil, fmt.Errorf("invalid block device: %w", err)
152 }
153
154 params.tbl.ID = params.DiskGUID
Tim Windelschmidt8e19fa42024-11-12 13:39:43 +0000155 params.tbl.BootCode = p.BIOSBootCode
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200156 params.efiPartition = &gpt.Partition{
157 Type: gpt.PartitionTypeEFISystem,
158 Name: ESPLabel,
159 }
160
161 if err := params.tbl.AddPartition(params.efiPartition, params.PartitionSize.ESP*Mi); err != nil {
162 return nil, fmt.Errorf("failed to allocate ESP: %w", err)
163 }
164
Jan Schärc1b6df42025-03-20 08:52:18 +0000165 if err := params.efiRoot.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), params.EFIPayload); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200166 return nil, err
167 }
168 // Place the A/B loader at the EFI bootloader autodiscovery path.
Jan Schärc1b6df42025-03-20 08:52:18 +0000169 if err := params.efiRoot.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.ABLoader); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200170 return nil, err
171 }
172 if params.NodeParameters != nil {
Jan Schärc1b6df42025-03-20 08:52:18 +0000173 if err := params.efiRoot.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200174 return nil, err
175 }
176 }
177
178 // Try to layout the fat32 partition. If it detects that the disk is too
179 // small, an error will be returned.
Jan Schärc1b6df42025-03-20 08:52:18 +0000180 if _, err := fat32.SizeFS(params.efiRoot, fat32.Options{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200181 BlockSize: uint16(params.efiPartition.BlockSize()),
182 BlockCount: uint32(params.efiPartition.BlockCount()),
183 Label: "MNGN_BOOT",
184 }); err != nil {
185 return nil, fmt.Errorf("failed to calculate size of FAT32: %w", err)
186 }
187
188 // Create the system partition only if its size is specified.
189 if params.PartitionSize.System != 0 && params.SystemImage != nil {
190 params.systemPartitionA = &gpt.Partition{
191 Type: SystemAType,
192 Name: SystemALabel,
193 }
194 if err := params.tbl.AddPartition(params.systemPartitionA, params.PartitionSize.System*Mi); err != nil {
195 return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
196 }
197 params.systemPartitionB = &gpt.Partition{
198 Type: SystemBType,
199 Name: SystemBLabel,
200 }
201 if err := params.tbl.AddPartition(params.systemPartitionB, params.PartitionSize.System*Mi); err != nil {
202 return nil, fmt.Errorf("failed to allocate system partition B: %w", err)
203 }
204 } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
205 // Safeguard against contradicting parameters.
206 return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
207 }
208 // Create the data partition only if its size is specified.
209 if params.PartitionSize.Data != 0 {
210 params.dataPartition = &gpt.Partition{
211 Type: DataType,
212 Name: DataLabel,
213 }
214 if err := params.tbl.AddPartition(params.dataPartition, -1); err != nil {
215 return nil, fmt.Errorf("failed to allocate data partition: %w", err)
216 }
217 }
218
219 return params, nil
220}
221
222const Mi = 1024 * 1024
223
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200224// Write writes a Metropolis OS image to a block device.
225func Write(params *Params) (*efivarfs.LoadOption, error) {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200226 p, err := Plan(params)
227 if err != nil {
228 return nil, err
229 }
230
231 return p.Apply()
232}