blob: 01c13ac3f024309a9821acb581dbead8603d2ef7 [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 (
Lorenz Brun35fcf032023-06-29 04:15:58 +020022 "bytes"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020023 "fmt"
24 "io"
Lorenz Brunad131882023-06-28 16:42:20 +020025 "strings"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020026
Mateusz Zalega612a0332021-11-17 20:04:52 +010027 "github.com/google/uuid"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020028
Lorenz Brunad131882023-06-28 16:42:20 +020029 "source.monogon.dev/metropolis/pkg/blockdev"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020030 "source.monogon.dev/metropolis/pkg/efivarfs"
Lorenz Brunad131882023-06-28 16:42:20 +020031 "source.monogon.dev/metropolis/pkg/fat32"
32 "source.monogon.dev/metropolis/pkg/gpt"
33)
34
35var (
36 SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c")
37 SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c")
38
39 DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966")
Mateusz Zalegac71efc92021-09-07 16:46:25 +020040)
41
42const (
Lorenz Brun35fcf032023-06-29 04:15:58 +020043 SystemALabel = "METROPOLIS-SYSTEM-A"
44 SystemBLabel = "METROPOLIS-SYSTEM-B"
45 DataLabel = "METROPOLIS-NODE-DATA"
46 ESPLabel = "ESP"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020047
48 EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
Lorenz Brun35fcf032023-06-29 04:15:58 +020049 EFIBootAPath = "/EFI/metropolis/boot-a.efi"
50 EFIBootBPath = "/EFI/metropolis/boot-b.efi"
Lorenz Brunad131882023-06-28 16:42:20 +020051 nodeParamsPath = "metropolis/parameters.pb"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020052)
53
Mateusz Zalegac71efc92021-09-07 16:46:25 +020054// PartitionSizeInfo contains parameters used during partition table
55// initialization and, in case of image files, space allocation.
56type PartitionSizeInfo struct {
57 // Size of the EFI System Partition (ESP), in mebibytes. The size must
58 // not be zero.
Lorenz Brunad131882023-06-28 16:42:20 +020059 ESP int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020060 // Size of the Metropolis system partition, in mebibytes. The partition
61 // won't be created if the size is zero.
Lorenz Brunad131882023-06-28 16:42:20 +020062 System int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020063 // Size of the Metropolis data partition, in mebibytes. The partition
64 // won't be created if the size is zero. If the image is output to a
65 // block device, the partition will be extended to fill the remaining
66 // space.
Lorenz Brunad131882023-06-28 16:42:20 +020067 Data int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020068}
69
70// Params contains parameters used by Create to build a Metropolis OS
71// image.
72type Params struct {
Lorenz Brunad131882023-06-28 16:42:20 +020073 // Output is the block device to which the OS image is written.
74 Output blockdev.BlockDev
Mateusz Zalegac71efc92021-09-07 16:46:25 +020075 // EFIPayload provides contents of the EFI payload file. It must not be
76 // nil.
Lorenz Brunad131882023-06-28 16:42:20 +020077 EFIPayload fat32.SizedReader
Mateusz Zalegac71efc92021-09-07 16:46:25 +020078 // SystemImage provides contents of the Metropolis system partition.
79 // If nil, no contents will be copied into the partition.
80 SystemImage io.Reader
81 // NodeParameters provides contents of the node parameters file. If nil,
82 // the node parameters file won't be created in the target ESP
83 // filesystem.
Lorenz Brunad131882023-06-28 16:42:20 +020084 NodeParameters fat32.SizedReader
85 // DiskGUID is a unique identifier of the image and a part of Table
Mateusz Zalegac71efc92021-09-07 16:46:25 +020086 // header. It's optional and can be left blank if the identifier is
87 // to be randomly generated. Setting it to a predetermined value can
88 // help in implementing reproducible builds.
Lorenz Brunad131882023-06-28 16:42:20 +020089 DiskGUID uuid.UUID
Mateusz Zalegac71efc92021-09-07 16:46:25 +020090 // PartitionSize specifies a size for the ESP, Metropolis System and
91 // Metropolis data partition.
92 PartitionSize PartitionSizeInfo
93}
94
Lorenz Brunad131882023-06-28 16:42:20 +020095const Mi = 1024 * 1024
96
97// Create writes a Metropolis OS image to a block device.
Lorenz Brunca1cff02023-06-26 17:52:44 +020098func Create(params *Params) (*efivarfs.LoadOption, error) {
Lorenz Brunad131882023-06-28 16:42:20 +020099 // Discard the entire device, we're going to write new data over it.
100 // Ignore errors, this is only advisory.
101 params.Output.Discard(0, params.Output.BlockCount())
102
103 tbl, err := gpt.New(params.Output)
104 if err != nil {
105 return nil, fmt.Errorf("invalid block device: %w", err)
106 }
107 tbl.ID = params.DiskGUID
108 esp := gpt.Partition{
109 Type: gpt.PartitionTypeEFISystem,
110 Name: ESPLabel,
111 }
112 if err := tbl.AddPartition(&esp, params.PartitionSize.ESP*Mi); err != nil {
113 return nil, fmt.Errorf("failed to allocate ESP: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200114 }
115
Lorenz Brunad131882023-06-28 16:42:20 +0200116 rootInode := fat32.Inode{
117 Attrs: fat32.AttrDirectory,
118 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200119 efiPayload, err := io.ReadAll(params.EFIPayload)
120 if err != nil {
121 return nil, fmt.Errorf("while reading EFIPayload: %w", err)
122 }
123 if err := rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), bytes.NewReader(efiPayload)); err != nil {
124 return nil, err
125 }
126 // Also place a copy of the boot file at the autodiscovery path. This will
127 // always boot slot A.
128 if err := rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), bytes.NewReader(efiPayload)); err != nil {
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200129 return nil, err
130 }
Lorenz Brunad131882023-06-28 16:42:20 +0200131 if params.NodeParameters != nil {
132 if err := rootInode.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
133 return nil, err
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200134 }
135 }
Lorenz Brunad131882023-06-28 16:42:20 +0200136 if err := fat32.WriteFS(blockdev.NewRWS(esp), rootInode, fat32.Options{
137 BlockSize: uint16(esp.BlockSize()),
138 BlockCount: uint32(esp.BlockCount()),
139 Label: "MNGN_BOOT",
140 }); err != nil {
141 return nil, fmt.Errorf("failed to write FAT32: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200142 }
Lorenz Brunad131882023-06-28 16:42:20 +0200143
144 // Create the system partition only if its size is specified.
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200145 if params.PartitionSize.System != 0 && params.SystemImage != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200146 systemPartitionA := gpt.Partition{
147 Type: SystemAType,
Lorenz Brun35fcf032023-06-29 04:15:58 +0200148 Name: SystemALabel,
Lorenz Brunad131882023-06-28 16:42:20 +0200149 }
150 if err := tbl.AddPartition(&systemPartitionA, params.PartitionSize.System*Mi); err != nil {
151 return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
152 }
153 if _, err := io.Copy(blockdev.NewRWS(systemPartitionA), params.SystemImage); err != nil {
154 return nil, fmt.Errorf("failed to write system partition A: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200155 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200156 systemPartitionB := gpt.Partition{
157 Type: SystemBType,
158 Name: SystemBLabel,
159 }
160 if err := tbl.AddPartition(&systemPartitionB, params.PartitionSize.System*Mi); err != nil {
161 return nil, fmt.Errorf("failed to allocate system partition B: %w", err)
162 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200163 } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
164 // Safeguard against contradicting parameters.
165 return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
166 }
Lorenz Brunad131882023-06-28 16:42:20 +0200167 // Create the data partition only if its size is specified.
168 if params.PartitionSize.Data != 0 {
169 dataPartition := gpt.Partition{
170 Type: DataType,
171 Name: DataLabel,
172 }
173 if err := tbl.AddPartition(&dataPartition, -1); err != nil {
174 return nil, fmt.Errorf("failed to allocate data partition: %w", err)
175 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200176 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200177
Lorenz Brunad131882023-06-28 16:42:20 +0200178 if err := tbl.Write(); err != nil {
179 return nil, fmt.Errorf("failed to write Table: %w", err)
Mateusz Zalega612a0332021-11-17 20:04:52 +0100180 }
Lorenz Brunad131882023-06-28 16:42:20 +0200181
182 // Build an EFI boot entry pointing to the image's ESP.
183 return &efivarfs.LoadOption{
184 Description: "Metropolis Slot A",
Lorenz Brunca1cff02023-06-26 17:52:44 +0200185 FilePath: efivarfs.DevicePath{
186 &efivarfs.HardDrivePath{
187 PartitionNumber: 1,
Lorenz Brunad131882023-06-28 16:42:20 +0200188 PartitionStartBlock: esp.FirstBlock,
189 PartitionSizeBlocks: esp.SizeBlocks(),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200190 PartitionMatch: efivarfs.PartitionGPT{
Lorenz Brunad131882023-06-28 16:42:20 +0200191 PartitionUUID: esp.ID,
Lorenz Brunca1cff02023-06-26 17:52:44 +0200192 },
193 },
Lorenz Brun35fcf032023-06-29 04:15:58 +0200194 efivarfs.FilePath(EFIBootAPath),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200195 },
Lorenz Brunad131882023-06-28 16:42:20 +0200196 }, nil
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200197}