blob: c79bf9eba03f4002480f39d8d37a225718782a56 [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
Jan Schär5fdca562025-04-14 11:33:29 +00005// OS image present in the installation medium's ESP, after which it reboots.
6// It's meant to be used as an init process.
Mateusz Zalega43e21072021-10-08 18:05:29 +02007package main
8
9import (
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020010 "context"
Lorenz Brun54a5a052023-10-02 16:40:11 +020011 _ "embed"
Lorenz Brun57d06a72022-01-13 14:12:27 +010012 "errors"
Mateusz Zalega43e21072021-10-08 18:05:29 +020013 "fmt"
Mateusz Zalega43e21072021-10-08 18:05:29 +020014 "os"
15 "path/filepath"
16 "strings"
Lorenz Brun57d06a72022-01-13 14:12:27 +010017 "time"
Mateusz Zalega43e21072021-10-08 18:05:29 +020018
19 "golang.org/x/sys/unix"
Serge Bazanski97783222021-12-14 16:04:26 +010020
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020021 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020022 "source.monogon.dev/osbase/bringup"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020023 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020024 "source.monogon.dev/osbase/efivarfs"
Jan Schär5fdca562025-04-14 11:33:29 +000025 "source.monogon.dev/osbase/oci"
26 ociosimage "source.monogon.dev/osbase/oci/osimage"
Jan Schärc1b6df42025-03-20 08:52:18 +000027 "source.monogon.dev/osbase/structfs"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020028 "source.monogon.dev/osbase/supervisor"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020029 "source.monogon.dev/osbase/sysfs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020030)
31
Jan Schär778cc332025-04-29 16:31:40 +000032//go:embed metropolis/node/core/abloader/abloader_bin.efi
Lorenz Brun54a5a052023-10-02 16:40:11 +020033var abloader []byte
34
Jan Schär10670e52025-04-23 12:54:48 +000035// Filled at linking time.
36var copyrightLine string
37
Mateusz Zalega43e21072021-10-08 18:05:29 +020038const mib = 1024 * 1024
39
Mateusz Zalega43e21072021-10-08 18:05:29 +020040// mountInstallerESP mounts the filesystem the installer was loaded from based
41// on espPath, which must point to the appropriate partition block device. The
42// filesystem is mounted at /installer.
43func mountInstallerESP(espPath string) error {
44 // Create the mountpoint.
45 if err := unix.Mkdir("/installer", 0700); err != nil {
46 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
47 }
48 // Mount the filesystem.
49 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
50 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
51 }
52 return nil
53}
54
55// findInstallableBlockDevices returns names of all the block devices suitable
56// for hosting a Metropolis installation, limited by the size expressed in
57// bytes minSize. The install medium espDev will be excluded from the result.
58func findInstallableBlockDevices(espDev string, minSize uint64) ([]string, error) {
59 // Use the partition's name to find and return the name of its parent
60 // device. It will be excluded from the list of suitable target devices.
61 srcDev, err := sysfs.ParentBlockDevice(espDev)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020062 if err != nil {
Tim Windelschmidt096654a2024-04-18 23:10:19 +020063 return nil, fmt.Errorf("failed to fetch parent device: %w", err)
64 }
Mateusz Zalega43e21072021-10-08 18:05:29 +020065 // Build the exclusion list containing forbidden handle prefixes.
66 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
67
68 // Get the block device handles by looking up directory contents.
69 const blkDirPath = "/sys/class/block"
70 blkDevs, err := os.ReadDir(blkDirPath)
71 if err != nil {
72 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
73 }
74 // Iterate over the handles, skipping any block device that either points to
75 // a partition, matches the exclusion list, or is smaller than minSize.
76 var suitable []string
77probeLoop:
78 for _, devInfo := range blkDevs {
79 // Skip devices according to the exclusion list.
80 for _, prefix := range exclude {
81 if strings.HasPrefix(devInfo.Name(), prefix) {
82 continue probeLoop
83 }
84 }
85
86 // Skip partition symlinks.
87 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
88 continue
89 } else if !os.IsNotExist(err) {
90 return nil, fmt.Errorf("while probing sysfs: %w", err)
91 }
92
93 // Skip devices of insufficient size.
94 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brunad131882023-06-28 16:42:20 +020095 dev, err := blockdev.Open(devPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +020096 if err != nil {
97 return nil, fmt.Errorf("couldn't open a block device at %q: %w", devPath, err)
98 }
Lorenz Brunad131882023-06-28 16:42:20 +020099 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200100 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +0200101 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +0200102 continue
103 }
104
105 suitable = append(suitable, devInfo.Name())
106 }
107 return suitable, nil
108}
109
Mateusz Zalega43e21072021-10-08 18:05:29 +0200110func main() {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200111 bringup.Runnable(installerRunnable).Run()
112}
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100113
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200114func installerRunnable(ctx context.Context) error {
115 l := supervisor.Logger(ctx)
116
117 l.Info("Metropolis Installer")
Jan Schär10670e52025-04-23 12:54:48 +0000118 l.Info(copyrightLine)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200119 l.Info("")
120
121 // Validate we are running via EFI.
122 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200123 // nolint:ST1005
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200124 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200125 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100126
Mateusz Zalega43e21072021-10-08 18:05:29 +0200127 // Read the installer ESP UUID from efivarfs.
128 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
129 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200130 return fmt.Errorf("while reading the installer ESP UUID: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200131 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100132 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
133 var espDev string
134 var retries = 30
135 for {
136 // Look up the installer partition based on espUuid.
137 espDev, err = sysfs.DeviceByPartUUID(espUuid)
138 if err == nil {
139 break
140 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
141 time.Sleep(1 * time.Second)
142 retries--
143 } else {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200144 return fmt.Errorf("while resolving the installer device handle: %w", err)
Lorenz Brun57d06a72022-01-13 14:12:27 +0100145 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200146 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100147 espPath := filepath.Join("/dev", espDev)
Jan Schär5fdca562025-04-14 11:33:29 +0000148 // Mount the installer partition. The OS image will be read from it.
Mateusz Zalega43e21072021-10-08 18:05:29 +0200149 if err := mountInstallerESP(espPath); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200150 return fmt.Errorf("while mounting the installer ESP: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200151 }
152
Jan Schärc1b6df42025-03-20 08:52:18 +0000153 nodeParameters, err := structfs.OSPathBlob("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100154 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200155 return fmt.Errorf("failed to open node parameters from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100156 }
157
Jan Schär5fdca562025-04-14 11:33:29 +0000158 ociImage, err := oci.ReadLayout("/installer/metropolis-installer/osimage")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100159 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000160 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100161 }
Jan Schär5fdca562025-04-14 11:33:29 +0000162 osImage, err := ociosimage.Read(ociImage)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100163 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000164 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100165 }
Jan Schär5fdca562025-04-14 11:33:29 +0000166
167 efiPayload, err := osImage.Payload("kernel.efi")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100168 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000169 return fmt.Errorf("cannot open EFI payload in OS image: %w", err)
170 }
171 systemImage, err := osImage.Payload("system")
172 if err != nil {
173 return fmt.Errorf("cannot open system image in OS image: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100174 }
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100175
Mateusz Zalega43e21072021-10-08 18:05:29 +0200176 // Build the osimage parameters.
177 installParams := osimage.Params{
178 PartitionSize: osimage.PartitionSizeInfo{
179 // ESP is the size of the node ESP partition, expressed in mebibytes.
Lorenz Brun35fcf032023-06-29 04:15:58 +0200180 ESP: 384,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200181 // System is the size of the node system partition, expressed in
182 // mebibytes.
183 System: 4096,
184 // Data must be nonzero in order for the data partition to be created.
185 // osimage will extend the data partition to fill all the available space
186 // whenever it's writing to block devices, such as now.
187 Data: 128,
188 },
Lorenz Brunad131882023-06-28 16:42:20 +0200189 SystemImage: systemImage,
Jan Schärc1b6df42025-03-20 08:52:18 +0000190 EFIPayload: efiPayload,
191 ABLoader: structfs.Bytes(abloader),
192 NodeParameters: nodeParameters,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200193 }
194 // Calculate the minimum target size based on the installation parameters.
195 minSize := uint64((installParams.PartitionSize.ESP +
Jan Schär42ef7c72024-03-18 15:09:51 +0100196 installParams.PartitionSize.System*2 +
Mateusz Zalega43e21072021-10-08 18:05:29 +0200197 installParams.PartitionSize.Data + 1) * mib)
198
199 // Look for suitable block devices, given the minimum size.
200 blkDevs, err := findInstallableBlockDevices(espDev, minSize)
201 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200202 return err
Mateusz Zalega43e21072021-10-08 18:05:29 +0200203 }
204 if len(blkDevs) == 0 {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200205 return fmt.Errorf("couldn't find a suitable block device")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200206 }
207 // Set the first suitable block device found as the installation target.
208 tgtBlkdevName := blkDevs[0]
209 // Update the osimage parameters with a path pointing at the target device.
210 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200211
212 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
213 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200214 return fmt.Errorf("error opening target device: %w", err)
Lorenz Brunad131882023-06-28 16:42:20 +0200215 }
216 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200217
218 // Use osimage to partition the target block device and set up its ESP.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200219 // Write will return an EFI boot entry on success.
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200220 l.Infof("Installing to %s...", tgtBlkdevPath)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200221 be, err := osimage.Write(&installParams)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200222 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200223 return fmt.Errorf("while installing: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200224 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200225
226 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200227 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200228 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200229 return fmt.Errorf("while creating a boot entry: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200230 }
231 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200232 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200233 return fmt.Errorf("while adjusting the boot order: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200234 }
235
236 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200237 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200238 unix.Sync()
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200239 l.Info("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200240 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200241 return nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200242}