blob: 2f497a82fe019f147500ed158e1507134e267c17 [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"
Lorenz Brunad131882023-06-28 16:42:20 +020024 "strings"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020025
Mateusz Zalega612a0332021-11-17 20:04:52 +010026 "github.com/google/uuid"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020027
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 "source.monogon.dev/osbase/blockdev"
29 "source.monogon.dev/osbase/efivarfs"
30 "source.monogon.dev/osbase/fat32"
31 "source.monogon.dev/osbase/gpt"
Lorenz Brunad131882023-06-28 16:42:20 +020032)
33
34var (
35 SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c")
36 SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c")
37
38 DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966")
Mateusz Zalegac71efc92021-09-07 16:46:25 +020039)
40
41const (
Lorenz Brun35fcf032023-06-29 04:15:58 +020042 SystemALabel = "METROPOLIS-SYSTEM-A"
43 SystemBLabel = "METROPOLIS-SYSTEM-B"
44 DataLabel = "METROPOLIS-NODE-DATA"
45 ESPLabel = "ESP"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020046
47 EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
Lorenz Brun35fcf032023-06-29 04:15:58 +020048 EFIBootAPath = "/EFI/metropolis/boot-a.efi"
49 EFIBootBPath = "/EFI/metropolis/boot-b.efi"
Lorenz Brunad131882023-06-28 16:42:20 +020050 nodeParamsPath = "metropolis/parameters.pb"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020051)
52
Mateusz Zalegac71efc92021-09-07 16:46:25 +020053// PartitionSizeInfo contains parameters used during partition table
54// initialization and, in case of image files, space allocation.
55type PartitionSizeInfo struct {
56 // Size of the EFI System Partition (ESP), in mebibytes. The size must
57 // not be zero.
Lorenz Brunad131882023-06-28 16:42:20 +020058 ESP int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020059 // Size of the Metropolis system partition, in mebibytes. The partition
60 // won't be created if the size is zero.
Lorenz Brunad131882023-06-28 16:42:20 +020061 System int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020062 // Size of the Metropolis data partition, in mebibytes. The partition
63 // won't be created if the size is zero. If the image is output to a
64 // block device, the partition will be extended to fill the remaining
65 // space.
Lorenz Brunad131882023-06-28 16:42:20 +020066 Data int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020067}
68
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020069// Params contains parameters used by Plan or Write to build a Metropolis OS
Mateusz Zalegac71efc92021-09-07 16:46:25 +020070// image.
71type Params struct {
Lorenz Brunad131882023-06-28 16:42:20 +020072 // Output is the block device to which the OS image is written.
73 Output blockdev.BlockDev
Lorenz Brun54a5a052023-10-02 16:40:11 +020074 // ABLoader provides the A/B loader which then loads the EFI loader for the
75 // correct slot.
76 ABLoader fat32.SizedReader
Mateusz Zalegac71efc92021-09-07 16:46:25 +020077 // EFIPayload provides contents of the EFI payload file. It must not be
Lorenz Brun54a5a052023-10-02 16:40:11 +020078 // nil. This gets put into boot slot A.
Lorenz Brunad131882023-06-28 16:42:20 +020079 EFIPayload fat32.SizedReader
Mateusz Zalegac71efc92021-09-07 16:46:25 +020080 // SystemImage provides contents of the Metropolis system partition.
81 // If nil, no contents will be copied into the partition.
82 SystemImage io.Reader
83 // NodeParameters provides contents of the node parameters file. If nil,
84 // the node parameters file won't be created in the target ESP
85 // filesystem.
Lorenz Brunad131882023-06-28 16:42:20 +020086 NodeParameters fat32.SizedReader
87 // DiskGUID is a unique identifier of the image and a part of Table
Mateusz Zalegac71efc92021-09-07 16:46:25 +020088 // header. It's optional and can be left blank if the identifier is
89 // to be randomly generated. Setting it to a predetermined value can
90 // help in implementing reproducible builds.
Lorenz Brunad131882023-06-28 16:42:20 +020091 DiskGUID uuid.UUID
Mateusz Zalegac71efc92021-09-07 16:46:25 +020092 // PartitionSize specifies a size for the ESP, Metropolis System and
93 // Metropolis data partition.
94 PartitionSize PartitionSizeInfo
Tim Windelschmidt8e19fa42024-11-12 13:39:43 +000095 // BIOSBootCode provides the optional contents for the protective MBR
96 // block which gets executed by legacy BIOS boot.
97 BIOSBootCode []byte
Mateusz Zalegac71efc92021-09-07 16:46:25 +020098}
99
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200100type plan struct {
101 *Params
102 rootInode fat32.Inode
103 tbl *gpt.Table
104 efiPartition *gpt.Partition
105 systemPartitionA *gpt.Partition
106 systemPartitionB *gpt.Partition
107 dataPartition *gpt.Partition
108}
Lorenz Brunad131882023-06-28 16:42:20 +0200109
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200110// Apply actually writes the planned installation to the blockdevice.
111func (i *plan) Apply() (*efivarfs.LoadOption, error) {
Lorenz Brunad131882023-06-28 16:42:20 +0200112 // Discard the entire device, we're going to write new data over it.
113 // Ignore errors, this is only advisory.
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200114 i.Output.Discard(0, i.Output.BlockCount()*i.Output.BlockSize())
Lorenz Brunad131882023-06-28 16:42:20 +0200115
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200116 if err := fat32.WriteFS(blockdev.NewRWS(i.efiPartition), i.rootInode, fat32.Options{
117 BlockSize: uint16(i.efiPartition.BlockSize()),
118 BlockCount: uint32(i.efiPartition.BlockCount()),
Lorenz Brunad131882023-06-28 16:42:20 +0200119 Label: "MNGN_BOOT",
120 }); err != nil {
121 return nil, fmt.Errorf("failed to write FAT32: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200122 }
Lorenz Brunad131882023-06-28 16:42:20 +0200123
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200124 if _, err := io.Copy(blockdev.NewRWS(i.systemPartitionA), i.SystemImage); err != nil {
125 return nil, fmt.Errorf("failed to write system partition A: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200126 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200127
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200128 if err := i.tbl.Write(); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200129 return nil, fmt.Errorf("failed to write Table: %w", err)
Mateusz Zalega612a0332021-11-17 20:04:52 +0100130 }
Lorenz Brunad131882023-06-28 16:42:20 +0200131
132 // Build an EFI boot entry pointing to the image's ESP.
133 return &efivarfs.LoadOption{
Lorenz Brun54a5a052023-10-02 16:40:11 +0200134 Description: "Metropolis",
Lorenz Brunca1cff02023-06-26 17:52:44 +0200135 FilePath: efivarfs.DevicePath{
136 &efivarfs.HardDrivePath{
137 PartitionNumber: 1,
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200138 PartitionStartBlock: i.efiPartition.FirstBlock,
139 PartitionSizeBlocks: i.efiPartition.SizeBlocks(),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200140 PartitionMatch: efivarfs.PartitionGPT{
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200141 PartitionUUID: i.efiPartition.ID,
Lorenz Brunca1cff02023-06-26 17:52:44 +0200142 },
143 },
Lorenz Brun54a5a052023-10-02 16:40:11 +0200144 efivarfs.FilePath(EFIPayloadPath),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200145 },
Lorenz Brunad131882023-06-28 16:42:20 +0200146 }, nil
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200147}
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200148
149// Plan allows to prepare an installation without modifying any data on the
150// system. To apply the planned installation, call Apply on the returned plan.
151func Plan(p *Params) (*plan, error) {
152 params := &plan{Params: p}
153
154 var err error
155 params.tbl, err = gpt.New(params.Output)
156 if err != nil {
157 return nil, fmt.Errorf("invalid block device: %w", err)
158 }
159
160 params.tbl.ID = params.DiskGUID
Tim Windelschmidt8e19fa42024-11-12 13:39:43 +0000161 params.tbl.BootCode = p.BIOSBootCode
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200162 params.efiPartition = &gpt.Partition{
163 Type: gpt.PartitionTypeEFISystem,
164 Name: ESPLabel,
165 }
166
167 if err := params.tbl.AddPartition(params.efiPartition, params.PartitionSize.ESP*Mi); err != nil {
168 return nil, fmt.Errorf("failed to allocate ESP: %w", err)
169 }
170
171 params.rootInode = fat32.Inode{
172 Attrs: fat32.AttrDirectory,
173 }
174 if err := params.rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), params.EFIPayload); err != nil {
175 return nil, err
176 }
177 // Place the A/B loader at the EFI bootloader autodiscovery path.
178 if err := params.rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.ABLoader); err != nil {
179 return nil, err
180 }
181 if params.NodeParameters != nil {
182 if err := params.rootInode.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
183 return nil, err
184 }
185 }
186
187 // Try to layout the fat32 partition. If it detects that the disk is too
188 // small, an error will be returned.
189 if _, err := fat32.SizeFS(params.rootInode, fat32.Options{
190 BlockSize: uint16(params.efiPartition.BlockSize()),
191 BlockCount: uint32(params.efiPartition.BlockCount()),
192 Label: "MNGN_BOOT",
193 }); err != nil {
194 return nil, fmt.Errorf("failed to calculate size of FAT32: %w", err)
195 }
196
197 // Create the system partition only if its size is specified.
198 if params.PartitionSize.System != 0 && params.SystemImage != nil {
199 params.systemPartitionA = &gpt.Partition{
200 Type: SystemAType,
201 Name: SystemALabel,
202 }
203 if err := params.tbl.AddPartition(params.systemPartitionA, params.PartitionSize.System*Mi); err != nil {
204 return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
205 }
206 params.systemPartitionB = &gpt.Partition{
207 Type: SystemBType,
208 Name: SystemBLabel,
209 }
210 if err := params.tbl.AddPartition(params.systemPartitionB, params.PartitionSize.System*Mi); err != nil {
211 return nil, fmt.Errorf("failed to allocate system partition B: %w", err)
212 }
213 } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
214 // Safeguard against contradicting parameters.
215 return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
216 }
217 // Create the data partition only if its size is specified.
218 if params.PartitionSize.Data != 0 {
219 params.dataPartition = &gpt.Partition{
220 Type: DataType,
221 Name: DataLabel,
222 }
223 if err := params.tbl.AddPartition(params.dataPartition, -1); err != nil {
224 return nil, fmt.Errorf("failed to allocate data partition: %w", err)
225 }
226 }
227
228 return params, nil
229}
230
231const Mi = 1024 * 1024
232
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200233// Write writes a Metropolis OS image to a block device.
234func Write(params *Params) (*efivarfs.LoadOption, error) {
Tim Windelschmidtbceb1602024-07-10 18:17:32 +0200235 p, err := Plan(params)
236 if err != nil {
237 return nil, err
238 }
239
240 return p.Apply()
241}