blob: 216f99cf47ac484b93c3377efea6289ae01734e2 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Mateusz Zalega43e21072021-10-08 18:05:29 +02002// SPDX-License-Identifier: Apache-2.0
Mateusz Zalega43e21072021-10-08 18:05:29 +02003
4// Installer creates a Metropolis image at a suitable block device based on the
5// installer bundle present in the installation medium's ESP, after which it
6// reboots. It's meant to be used as an init process.
7package main
8
9import (
Lorenz Brun0b93c8d2021-11-09 03:58:40 +010010 "archive/zip"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020011 "context"
Lorenz Brun54a5a052023-10-02 16:40:11 +020012 _ "embed"
Lorenz Brun57d06a72022-01-13 14:12:27 +010013 "errors"
Mateusz Zalega43e21072021-10-08 18:05:29 +020014 "fmt"
Mateusz Zalega43e21072021-10-08 18:05:29 +020015 "os"
16 "path/filepath"
17 "strings"
Lorenz Brun57d06a72022-01-13 14:12:27 +010018 "time"
Mateusz Zalega43e21072021-10-08 18:05:29 +020019
20 "golang.org/x/sys/unix"
Serge Bazanski97783222021-12-14 16:04:26 +010021
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020022 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020023 "source.monogon.dev/osbase/bringup"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020024 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020025 "source.monogon.dev/osbase/efivarfs"
Jan Schärc1b6df42025-03-20 08:52:18 +000026 "source.monogon.dev/osbase/structfs"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020027 "source.monogon.dev/osbase/supervisor"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 "source.monogon.dev/osbase/sysfs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020029)
30
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +020031//go:embed metropolis/node/core/abloader/abloader.efi
Lorenz Brun54a5a052023-10-02 16:40:11 +020032var abloader []byte
33
Mateusz Zalega43e21072021-10-08 18:05:29 +020034const mib = 1024 * 1024
35
Mateusz Zalega43e21072021-10-08 18:05:29 +020036// mountInstallerESP mounts the filesystem the installer was loaded from based
37// on espPath, which must point to the appropriate partition block device. The
38// filesystem is mounted at /installer.
39func mountInstallerESP(espPath string) error {
40 // Create the mountpoint.
41 if err := unix.Mkdir("/installer", 0700); err != nil {
42 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
43 }
44 // Mount the filesystem.
45 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
46 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
47 }
48 return nil
49}
50
51// findInstallableBlockDevices returns names of all the block devices suitable
52// for hosting a Metropolis installation, limited by the size expressed in
53// bytes minSize. The install medium espDev will be excluded from the result.
54func findInstallableBlockDevices(espDev string, minSize uint64) ([]string, error) {
55 // Use the partition's name to find and return the name of its parent
56 // device. It will be excluded from the list of suitable target devices.
57 srcDev, err := sysfs.ParentBlockDevice(espDev)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020058 if err != nil {
Tim Windelschmidt096654a2024-04-18 23:10:19 +020059 return nil, fmt.Errorf("failed to fetch parent device: %w", err)
60 }
Mateusz Zalega43e21072021-10-08 18:05:29 +020061 // Build the exclusion list containing forbidden handle prefixes.
62 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
63
64 // Get the block device handles by looking up directory contents.
65 const blkDirPath = "/sys/class/block"
66 blkDevs, err := os.ReadDir(blkDirPath)
67 if err != nil {
68 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
69 }
70 // Iterate over the handles, skipping any block device that either points to
71 // a partition, matches the exclusion list, or is smaller than minSize.
72 var suitable []string
73probeLoop:
74 for _, devInfo := range blkDevs {
75 // Skip devices according to the exclusion list.
76 for _, prefix := range exclude {
77 if strings.HasPrefix(devInfo.Name(), prefix) {
78 continue probeLoop
79 }
80 }
81
82 // Skip partition symlinks.
83 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
84 continue
85 } else if !os.IsNotExist(err) {
86 return nil, fmt.Errorf("while probing sysfs: %w", err)
87 }
88
89 // Skip devices of insufficient size.
90 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brunad131882023-06-28 16:42:20 +020091 dev, err := blockdev.Open(devPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +020092 if err != nil {
93 return nil, fmt.Errorf("couldn't open a block device at %q: %w", devPath, err)
94 }
Lorenz Brunad131882023-06-28 16:42:20 +020095 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +020096 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +020097 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +020098 continue
99 }
100
101 suitable = append(suitable, devInfo.Name())
102 }
103 return suitable, nil
104}
105
Jan Schärc1b6df42025-03-20 08:52:18 +0000106// zipBlob looks up a file in a [zip.Reader] and adapts it to [structfs.Blob].
107func zipBlob(reader *zip.Reader, name string) (zipFileBlob, error) {
108 for _, file := range reader.File {
109 if file.Name == name {
110 return zipFileBlob{file}, nil
111 }
112 }
113 return zipFileBlob{}, fmt.Errorf("file %q not found", name)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200114}
115
Jan Schärc1b6df42025-03-20 08:52:18 +0000116type zipFileBlob struct {
117 *zip.File
118}
119
120func (f zipFileBlob) Size() int64 {
121 return int64(f.File.UncompressedSize64)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200122}
123
124func main() {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200125 bringup.Runnable(installerRunnable).Run()
126}
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100127
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200128func installerRunnable(ctx context.Context) error {
129 l := supervisor.Logger(ctx)
130
131 l.Info("Metropolis Installer")
132 l.Info("Copyright (c) 2024 The Monogon Project Authors")
133 l.Info("")
134
135 // Validate we are running via EFI.
136 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200137 // nolint:ST1005
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200138 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200139 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100140
Mateusz Zalega43e21072021-10-08 18:05:29 +0200141 // Read the installer ESP UUID from efivarfs.
142 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
143 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200144 return fmt.Errorf("while reading the installer ESP UUID: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200145 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100146 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
147 var espDev string
148 var retries = 30
149 for {
150 // Look up the installer partition based on espUuid.
151 espDev, err = sysfs.DeviceByPartUUID(espUuid)
152 if err == nil {
153 break
154 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
155 time.Sleep(1 * time.Second)
156 retries--
157 } else {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200158 return fmt.Errorf("while resolving the installer device handle: %w", err)
Lorenz Brun57d06a72022-01-13 14:12:27 +0100159 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200160 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100161 espPath := filepath.Join("/dev", espDev)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200162 // Mount the installer partition. The installer bundle will be read from it.
163 if err := mountInstallerESP(espPath); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200164 return fmt.Errorf("while mounting the installer ESP: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200165 }
166
Jan Schärc1b6df42025-03-20 08:52:18 +0000167 nodeParameters, err := structfs.OSPathBlob("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100168 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200169 return fmt.Errorf("failed to open node parameters from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100170 }
171
172 // TODO(lorenz): Replace with proper bundles
Lorenz Brun6c35e972021-12-14 03:08:23 +0100173 bundle, err := zip.OpenReader("/installer/metropolis-installer/bundle.bin")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100174 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200175 return fmt.Errorf("failed to open node bundle from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100176 }
177 defer bundle.Close()
Jan Schärc1b6df42025-03-20 08:52:18 +0000178 efiPayload, err := zipBlob(&bundle.Reader, "kernel_efi.efi")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100179 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200180 return fmt.Errorf("cannot open EFI payload in bundle: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100181 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000182 systemImage, err := zipBlob(&bundle.Reader, "verity_rootfs.img")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100183 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200184 return fmt.Errorf("cannot open system image in bundle: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100185 }
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100186
Mateusz Zalega43e21072021-10-08 18:05:29 +0200187 // Build the osimage parameters.
188 installParams := osimage.Params{
189 PartitionSize: osimage.PartitionSizeInfo{
190 // ESP is the size of the node ESP partition, expressed in mebibytes.
Lorenz Brun35fcf032023-06-29 04:15:58 +0200191 ESP: 384,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200192 // System is the size of the node system partition, expressed in
193 // mebibytes.
194 System: 4096,
195 // Data must be nonzero in order for the data partition to be created.
196 // osimage will extend the data partition to fill all the available space
197 // whenever it's writing to block devices, such as now.
198 Data: 128,
199 },
Lorenz Brunad131882023-06-28 16:42:20 +0200200 SystemImage: systemImage,
Jan Schärc1b6df42025-03-20 08:52:18 +0000201 EFIPayload: efiPayload,
202 ABLoader: structfs.Bytes(abloader),
203 NodeParameters: nodeParameters,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200204 }
205 // Calculate the minimum target size based on the installation parameters.
206 minSize := uint64((installParams.PartitionSize.ESP +
Jan Schär42ef7c72024-03-18 15:09:51 +0100207 installParams.PartitionSize.System*2 +
Mateusz Zalega43e21072021-10-08 18:05:29 +0200208 installParams.PartitionSize.Data + 1) * mib)
209
210 // Look for suitable block devices, given the minimum size.
211 blkDevs, err := findInstallableBlockDevices(espDev, minSize)
212 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200213 return err
Mateusz Zalega43e21072021-10-08 18:05:29 +0200214 }
215 if len(blkDevs) == 0 {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200216 return fmt.Errorf("couldn't find a suitable block device")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200217 }
218 // Set the first suitable block device found as the installation target.
219 tgtBlkdevName := blkDevs[0]
220 // Update the osimage parameters with a path pointing at the target device.
221 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200222
223 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
224 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200225 return fmt.Errorf("error opening target device: %w", err)
Lorenz Brunad131882023-06-28 16:42:20 +0200226 }
227 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200228
229 // Use osimage to partition the target block device and set up its ESP.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200230 // Write will return an EFI boot entry on success.
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200231 l.Infof("Installing to %s...", tgtBlkdevPath)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200232 be, err := osimage.Write(&installParams)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200233 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200234 return fmt.Errorf("while installing: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200235 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200236
237 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200238 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200239 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200240 return fmt.Errorf("while creating a boot entry: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200241 }
242 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200243 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200244 return fmt.Errorf("while adjusting the boot order: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200245 }
246
247 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200248 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200249 unix.Sync()
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200250 l.Info("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200251 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200252 return nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200253}