blob: 4d93723ab001106a602ded2b00b0ec15174649c2 [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
Jan Schäre19d2792025-06-23 12:37:58 +00004// Package install allows planning and executing the installation of Metropolis
5// to a block device.
6package install
Mateusz Zalegac71efc92021-09-07 16:46:25 +02007
8import (
Jan Schärf6136ca2025-07-01 08:38:07 +00009 _ "embed"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020010 "fmt"
11 "io"
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ärdaf9e952025-06-23 13:28:16 +000019 "source.monogon.dev/osbase/oci/osimage"
Jan Schärc1b6df42025-03-20 08:52:18 +000020 "source.monogon.dev/osbase/structfs"
Lorenz Brunad131882023-06-28 16:42:20 +020021)
22
23var (
24 SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c")
25 SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c")
26
27 DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966")
Mateusz Zalegac71efc92021-09-07 16:46:25 +020028)
29
30const (
Lorenz Brun35fcf032023-06-29 04:15:58 +020031 SystemALabel = "METROPOLIS-SYSTEM-A"
32 SystemBLabel = "METROPOLIS-SYSTEM-B"
33 DataLabel = "METROPOLIS-NODE-DATA"
34 ESPLabel = "ESP"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020035
Jan Schär091b8a62025-05-13 09:07:50 +000036 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
Jan Schär4b888262025-05-13 09:12:03 +000041var EFIBootName = map[string]string{
42 "x86_64": "BOOTx64.EFI",
43 "aarch64": "BOOTAA64.EFI",
44}
45
46// EFIBootPath returns the default file path according to the UEFI Specification
47// v2.11 Section 3.5.1.1. This file is booted by any compliant UEFI firmware in
48// absence of another bootable boot entry.
49func EFIBootPath(architecture string) (string, error) {
50 bootName, ok := EFIBootName[architecture]
51 if !ok {
52 return "", fmt.Errorf("unsupported architecture %q", architecture)
53 }
54 return "EFI/BOOT/" + bootName, nil
55}
56
Jan Schärf6136ca2025-07-01 08:38:07 +000057//go:embed metropolis/node/bios_bootcode/boot.bin
58var BootcodeX86 []byte
59
Mateusz Zalegac71efc92021-09-07 16:46:25 +020060// PartitionSizeInfo contains parameters used during partition table
61// initialization and, in case of image files, space allocation.
62type PartitionSizeInfo struct {
63 // Size of the EFI System Partition (ESP), in mebibytes. The size must
64 // not be zero.
Lorenz Brunad131882023-06-28 16:42:20 +020065 ESP int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020066 // Size of the Metropolis system partition, in mebibytes. The partition
67 // won't be created if the size is zero.
Lorenz Brunad131882023-06-28 16:42:20 +020068 System int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020069 // Size of the Metropolis data partition, in mebibytes. The partition
70 // won't be created if the size is zero. If the image is output to a
71 // block device, the partition will be extended to fill the remaining
72 // space.
Lorenz Brunad131882023-06-28 16:42:20 +020073 Data int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020074}
75
Jan Schäre19d2792025-06-23 12:37:58 +000076// Params contains parameters used by Plan or Write to install Metropolis OS.
Mateusz Zalegac71efc92021-09-07 16:46:25 +020077type Params struct {
Jan Schäre19d2792025-06-23 12:37:58 +000078 // Output is the block device to which the OS is installed.
Lorenz Brunad131882023-06-28 16:42:20 +020079 Output blockdev.BlockDev
Jan Schärdaf9e952025-06-23 13:28:16 +000080 // OSImage is the image from which the OS is installed.
81 OSImage *osimage.Image
82 // UnverifiedPayloads disables verification of payloads if set.
83 // This only works with uncompressed OS images.
84 UnverifiedPayloads bool
Lorenz Brun54a5a052023-10-02 16:40:11 +020085 // ABLoader provides the A/B loader which then loads the EFI loader for the
86 // correct slot.
Jan Schärc1b6df42025-03-20 08:52:18 +000087 ABLoader structfs.Blob
Mateusz Zalegac71efc92021-09-07 16:46:25 +020088 // NodeParameters provides contents of the node parameters file. If nil,
89 // the node parameters file won't be created in the target ESP
90 // filesystem.
Jan Schärc1b6df42025-03-20 08:52:18 +000091 NodeParameters structfs.Blob
Lorenz Brunad131882023-06-28 16:42:20 +020092 // DiskGUID is a unique identifier of the image and a part of Table
Mateusz Zalegac71efc92021-09-07 16:46:25 +020093 // header. It's optional and can be left blank if the identifier is
94 // to be randomly generated. Setting it to a predetermined value can
95 // help in implementing reproducible builds.
Lorenz Brunad131882023-06-28 16:42:20 +020096 DiskGUID uuid.UUID
Mateusz Zalegac71efc92021-09-07 16:46:25 +020097 // PartitionSize specifies a size for the ESP, Metropolis System and
98 // Metropolis data partition.
99 PartitionSize PartitionSizeInfo
100}
101
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200102type plan struct {
103 *Params
Jan Schärdaf9e952025-06-23 13:28:16 +0000104 systemImage structfs.Blob
Jan Schär4b888262025-05-13 09:12:03 +0000105 efiBootPath string
Jan Schärc1b6df42025-03-20 08:52:18 +0000106 efiRoot structfs.Tree
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200107 tbl *gpt.Table
108 efiPartition *gpt.Partition
109 systemPartitionA *gpt.Partition
110 systemPartitionB *gpt.Partition
111 dataPartition *gpt.Partition
112}
Lorenz Brunad131882023-06-28 16:42:20 +0200113
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200114// Apply actually writes the planned installation to the blockdevice.
115func (i *plan) Apply() (*efivarfs.LoadOption, error) {
Lorenz Brunad131882023-06-28 16:42:20 +0200116 // Discard the entire device, we're going to write new data over it.
117 // Ignore errors, this is only advisory.
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200118 i.Output.Discard(0, i.Output.BlockCount()*i.Output.BlockSize())
Lorenz Brunad131882023-06-28 16:42:20 +0200119
Jan Schärc1b6df42025-03-20 08:52:18 +0000120 if err := fat32.WriteFS(blockdev.NewRWS(i.efiPartition), i.efiRoot, fat32.Options{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200121 BlockSize: uint16(i.efiPartition.BlockSize()),
122 BlockCount: uint32(i.efiPartition.BlockCount()),
Lorenz Brunad131882023-06-28 16:42:20 +0200123 Label: "MNGN_BOOT",
124 }); err != nil {
125 return nil, fmt.Errorf("failed to write FAT32: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200126 }
Lorenz Brunad131882023-06-28 16:42:20 +0200127
Jan Schärdaf9e952025-06-23 13:28:16 +0000128 systemImage, err := i.systemImage.Open()
Jan Schärc1b6df42025-03-20 08:52:18 +0000129 if err != nil {
130 return nil, fmt.Errorf("failed to open system image: %w", err)
131 }
Jan Schärdaf9e952025-06-23 13:28:16 +0000132 if _, err := io.CopyN(blockdev.NewRWS(i.systemPartitionA), systemImage, i.systemImage.Size()); err != nil {
Jan Schärc1b6df42025-03-20 08:52:18 +0000133 systemImage.Close()
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200134 return nil, fmt.Errorf("failed to write system partition A: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200135 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000136 systemImage.Close()
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200137
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200138 if err := i.tbl.Write(); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200139 return nil, fmt.Errorf("failed to write Table: %w", err)
Mateusz Zalega612a0332021-11-17 20:04:52 +0100140 }
Lorenz Brunad131882023-06-28 16:42:20 +0200141
142 // Build an EFI boot entry pointing to the image's ESP.
143 return &efivarfs.LoadOption{
Lorenz Brun54a5a052023-10-02 16:40:11 +0200144 Description: "Metropolis",
Lorenz Brunca1cff02023-06-26 17:52:44 +0200145 FilePath: efivarfs.DevicePath{
146 &efivarfs.HardDrivePath{
147 PartitionNumber: 1,
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200148 PartitionStartBlock: i.efiPartition.FirstBlock,
149 PartitionSizeBlocks: i.efiPartition.SizeBlocks(),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200150 PartitionMatch: efivarfs.PartitionGPT{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200151 PartitionUUID: i.efiPartition.ID,
Lorenz Brunca1cff02023-06-26 17:52:44 +0200152 },
153 },
Jan Schär4b888262025-05-13 09:12:03 +0000154 efivarfs.FilePath("/" + i.efiBootPath),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200155 },
Lorenz Brunad131882023-06-28 16:42:20 +0200156 }, nil
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200157}
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200158
159// Plan allows to prepare an installation without modifying any data on the
160// system. To apply the planned installation, call Apply on the returned plan.
161func Plan(p *Params) (*plan, error) {
162 params := &plan{Params: p}
163
Jan Schärdaf9e952025-06-23 13:28:16 +0000164 payload := p.OSImage.Payload
165 if p.UnverifiedPayloads {
166 payload = p.OSImage.PayloadUnverified
167 }
168 efiPayload, err := payload("kernel.efi")
169 if err != nil {
170 return nil, fmt.Errorf("cannot open EFI payload in OS image: %w", err)
171 }
172 params.systemImage, err = payload("system")
173 if err != nil {
174 return nil, fmt.Errorf("cannot open system image in OS image: %w", err)
175 }
176
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200177 params.tbl, err = gpt.New(params.Output)
178 if err != nil {
179 return nil, fmt.Errorf("invalid block device: %w", err)
180 }
181
182 params.tbl.ID = params.DiskGUID
Jan Schärf6136ca2025-07-01 08:38:07 +0000183 architecture := p.OSImage.Config.ProductInfo.Architecture()
184 if architecture == "x86_64" {
185 params.tbl.BootCode = BootcodeX86
186 }
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200187 params.efiPartition = &gpt.Partition{
188 Type: gpt.PartitionTypeEFISystem,
189 Name: ESPLabel,
190 }
191
192 if err := params.tbl.AddPartition(params.efiPartition, params.PartitionSize.ESP*Mi); err != nil {
193 return nil, fmt.Errorf("failed to allocate ESP: %w", err)
194 }
195
Jan Schärdaf9e952025-06-23 13:28:16 +0000196 if err := params.efiRoot.PlaceFile(EFIBootAPath, efiPayload); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200197 return nil, err
198 }
199 // Place the A/B loader at the EFI bootloader autodiscovery path.
Jan Schärf6136ca2025-07-01 08:38:07 +0000200 params.efiBootPath, err = EFIBootPath(architecture)
Jan Schär4b888262025-05-13 09:12:03 +0000201 if err != nil {
202 return nil, err
203 }
204 if err := params.efiRoot.PlaceFile(params.efiBootPath, params.ABLoader); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200205 return nil, err
206 }
207 if params.NodeParameters != nil {
Jan Schärc1b6df42025-03-20 08:52:18 +0000208 if err := params.efiRoot.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200209 return nil, err
210 }
211 }
212
213 // Try to layout the fat32 partition. If it detects that the disk is too
214 // small, an error will be returned.
Jan Schärc1b6df42025-03-20 08:52:18 +0000215 if _, err := fat32.SizeFS(params.efiRoot, fat32.Options{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200216 BlockSize: uint16(params.efiPartition.BlockSize()),
217 BlockCount: uint32(params.efiPartition.BlockCount()),
218 Label: "MNGN_BOOT",
219 }); err != nil {
220 return nil, fmt.Errorf("failed to calculate size of FAT32: %w", err)
221 }
222
Jan Schärdaf9e952025-06-23 13:28:16 +0000223 // Create the system partition.
224 params.systemPartitionA = &gpt.Partition{
225 Type: SystemAType,
226 Name: SystemALabel,
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200227 }
Jan Schärdaf9e952025-06-23 13:28:16 +0000228 if err := params.tbl.AddPartition(params.systemPartitionA, params.PartitionSize.System*Mi); err != nil {
229 return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
230 }
231 params.systemPartitionB = &gpt.Partition{
232 Type: SystemBType,
233 Name: SystemBLabel,
234 }
235 if err := params.tbl.AddPartition(params.systemPartitionB, params.PartitionSize.System*Mi); err != nil {
236 return nil, fmt.Errorf("failed to allocate system partition B: %w", err)
237 }
238
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200239 // Create the data partition only if its size is specified.
240 if params.PartitionSize.Data != 0 {
241 params.dataPartition = &gpt.Partition{
242 Type: DataType,
243 Name: DataLabel,
244 }
245 if err := params.tbl.AddPartition(params.dataPartition, -1); err != nil {
246 return nil, fmt.Errorf("failed to allocate data partition: %w", err)
247 }
248 }
249
250 return params, nil
251}
252
253const Mi = 1024 * 1024
254
Jan Schäre19d2792025-06-23 12:37:58 +0000255// Write installs Metropolis OS to a block device.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200256func Write(params *Params) (*efivarfs.LoadOption, error) {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200257 p, err := Plan(params)
258 if err != nil {
259 return nil, err
260 }
261
262 return p.Apply()
263}