blob: 709843960b234121d4953232683350de578e0505 [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
Lorenz Brunad131882023-06-28 16:42:20 +020028 "source.monogon.dev/metropolis/pkg/blockdev"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020029 "source.monogon.dev/metropolis/pkg/efivarfs"
Lorenz Brunad131882023-06-28 16:42:20 +020030 "source.monogon.dev/metropolis/pkg/fat32"
31 "source.monogon.dev/metropolis/pkg/gpt"
32)
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 Brunad131882023-06-28 16:42:20 +020042 SystemLabel = "METROPOLIS-SYSTEM"
43 DataLabel = "METROPOLIS-NODE-DATA"
44 ESPLabel = "ESP"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020045
46 EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
Lorenz Brunad131882023-06-28 16:42:20 +020047 nodeParamsPath = "metropolis/parameters.pb"
Mateusz Zalegac71efc92021-09-07 16:46:25 +020048)
49
Mateusz Zalegac71efc92021-09-07 16:46:25 +020050// PartitionSizeInfo contains parameters used during partition table
51// initialization and, in case of image files, space allocation.
52type PartitionSizeInfo struct {
53 // Size of the EFI System Partition (ESP), in mebibytes. The size must
54 // not be zero.
Lorenz Brunad131882023-06-28 16:42:20 +020055 ESP int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020056 // Size of the Metropolis system partition, in mebibytes. The partition
57 // won't be created if the size is zero.
Lorenz Brunad131882023-06-28 16:42:20 +020058 System int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020059 // Size of the Metropolis data partition, in mebibytes. The partition
60 // won't be created if the size is zero. If the image is output to a
61 // block device, the partition will be extended to fill the remaining
62 // space.
Lorenz Brunad131882023-06-28 16:42:20 +020063 Data int64
Mateusz Zalegac71efc92021-09-07 16:46:25 +020064}
65
66// Params contains parameters used by Create to build a Metropolis OS
67// image.
68type Params struct {
Lorenz Brunad131882023-06-28 16:42:20 +020069 // Output is the block device to which the OS image is written.
70 Output blockdev.BlockDev
Mateusz Zalegac71efc92021-09-07 16:46:25 +020071 // EFIPayload provides contents of the EFI payload file. It must not be
72 // nil.
Lorenz Brunad131882023-06-28 16:42:20 +020073 EFIPayload fat32.SizedReader
Mateusz Zalegac71efc92021-09-07 16:46:25 +020074 // SystemImage provides contents of the Metropolis system partition.
75 // If nil, no contents will be copied into the partition.
76 SystemImage io.Reader
77 // NodeParameters provides contents of the node parameters file. If nil,
78 // the node parameters file won't be created in the target ESP
79 // filesystem.
Lorenz Brunad131882023-06-28 16:42:20 +020080 NodeParameters fat32.SizedReader
81 // DiskGUID is a unique identifier of the image and a part of Table
Mateusz Zalegac71efc92021-09-07 16:46:25 +020082 // header. It's optional and can be left blank if the identifier is
83 // to be randomly generated. Setting it to a predetermined value can
84 // help in implementing reproducible builds.
Lorenz Brunad131882023-06-28 16:42:20 +020085 DiskGUID uuid.UUID
Mateusz Zalegac71efc92021-09-07 16:46:25 +020086 // PartitionSize specifies a size for the ESP, Metropolis System and
87 // Metropolis data partition.
88 PartitionSize PartitionSizeInfo
89}
90
Lorenz Brunad131882023-06-28 16:42:20 +020091const Mi = 1024 * 1024
92
93// Create writes a Metropolis OS image to a block device.
Lorenz Brunca1cff02023-06-26 17:52:44 +020094func Create(params *Params) (*efivarfs.LoadOption, error) {
Lorenz Brunad131882023-06-28 16:42:20 +020095 // Discard the entire device, we're going to write new data over it.
96 // Ignore errors, this is only advisory.
97 params.Output.Discard(0, params.Output.BlockCount())
98
99 tbl, err := gpt.New(params.Output)
100 if err != nil {
101 return nil, fmt.Errorf("invalid block device: %w", err)
102 }
103 tbl.ID = params.DiskGUID
104 esp := gpt.Partition{
105 Type: gpt.PartitionTypeEFISystem,
106 Name: ESPLabel,
107 }
108 if err := tbl.AddPartition(&esp, params.PartitionSize.ESP*Mi); err != nil {
109 return nil, fmt.Errorf("failed to allocate ESP: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200110 }
111
Lorenz Brunad131882023-06-28 16:42:20 +0200112 rootInode := fat32.Inode{
113 Attrs: fat32.AttrDirectory,
114 }
115 if err := rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.EFIPayload); err != nil {
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200116 return nil, err
117 }
Lorenz Brunad131882023-06-28 16:42:20 +0200118 if params.NodeParameters != nil {
119 if err := rootInode.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
120 return nil, err
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200121 }
122 }
Lorenz Brunad131882023-06-28 16:42:20 +0200123 if err := fat32.WriteFS(blockdev.NewRWS(esp), rootInode, fat32.Options{
124 BlockSize: uint16(esp.BlockSize()),
125 BlockCount: uint32(esp.BlockCount()),
126 Label: "MNGN_BOOT",
127 }); err != nil {
128 return nil, fmt.Errorf("failed to write FAT32: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200129 }
Lorenz Brunad131882023-06-28 16:42:20 +0200130
131 // Create the system partition only if its size is specified.
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200132 if params.PartitionSize.System != 0 && params.SystemImage != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200133 systemPartitionA := gpt.Partition{
134 Type: SystemAType,
135 Name: SystemLabel,
136 }
137 if err := tbl.AddPartition(&systemPartitionA, params.PartitionSize.System*Mi); err != nil {
138 return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
139 }
140 if _, err := io.Copy(blockdev.NewRWS(systemPartitionA), params.SystemImage); err != nil {
141 return nil, fmt.Errorf("failed to write system partition A: %w", err)
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200142 }
143 } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
144 // Safeguard against contradicting parameters.
145 return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
146 }
Lorenz Brunad131882023-06-28 16:42:20 +0200147 // Create the data partition only if its size is specified.
148 if params.PartitionSize.Data != 0 {
149 dataPartition := gpt.Partition{
150 Type: DataType,
151 Name: DataLabel,
152 }
153 if err := tbl.AddPartition(&dataPartition, -1); err != nil {
154 return nil, fmt.Errorf("failed to allocate data partition: %w", err)
155 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200156 }
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200157
Lorenz Brunad131882023-06-28 16:42:20 +0200158 if err := tbl.Write(); err != nil {
159 return nil, fmt.Errorf("failed to write Table: %w", err)
Mateusz Zalega612a0332021-11-17 20:04:52 +0100160 }
Lorenz Brunad131882023-06-28 16:42:20 +0200161
162 // Build an EFI boot entry pointing to the image's ESP.
163 return &efivarfs.LoadOption{
164 Description: "Metropolis Slot A",
Lorenz Brunca1cff02023-06-26 17:52:44 +0200165 FilePath: efivarfs.DevicePath{
166 &efivarfs.HardDrivePath{
167 PartitionNumber: 1,
Lorenz Brunad131882023-06-28 16:42:20 +0200168 PartitionStartBlock: esp.FirstBlock,
169 PartitionSizeBlocks: esp.SizeBlocks(),
Lorenz Brunca1cff02023-06-26 17:52:44 +0200170 PartitionMatch: efivarfs.PartitionGPT{
Lorenz Brunad131882023-06-28 16:42:20 +0200171 PartitionUUID: esp.ID,
Lorenz Brunca1cff02023-06-26 17:52:44 +0200172 },
173 },
174 efivarfs.FilePath(EFIPayloadPath),
175 },
Lorenz Brunad131882023-06-28 16:42:20 +0200176 }, nil
Mateusz Zalegac71efc92021-09-07 16:46:25 +0200177}