blob: ad0fa30658f4ae3b47f6a8db7938c92d84b87f4d [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
Mateusz Zalega43e21072021-10-08 18:05:29 +020035const mib = 1024 * 1024
36
Mateusz Zalega43e21072021-10-08 18:05:29 +020037// mountInstallerESP mounts the filesystem the installer was loaded from based
38// on espPath, which must point to the appropriate partition block device. The
39// filesystem is mounted at /installer.
40func mountInstallerESP(espPath string) error {
41 // Create the mountpoint.
42 if err := unix.Mkdir("/installer", 0700); err != nil {
43 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
44 }
45 // Mount the filesystem.
46 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
47 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
48 }
49 return nil
50}
51
52// findInstallableBlockDevices returns names of all the block devices suitable
53// for hosting a Metropolis installation, limited by the size expressed in
54// bytes minSize. The install medium espDev will be excluded from the result.
55func findInstallableBlockDevices(espDev string, minSize uint64) ([]string, error) {
56 // Use the partition's name to find and return the name of its parent
57 // device. It will be excluded from the list of suitable target devices.
58 srcDev, err := sysfs.ParentBlockDevice(espDev)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020059 if err != nil {
Tim Windelschmidt096654a2024-04-18 23:10:19 +020060 return nil, fmt.Errorf("failed to fetch parent device: %w", err)
61 }
Mateusz Zalega43e21072021-10-08 18:05:29 +020062 // Build the exclusion list containing forbidden handle prefixes.
63 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
64
65 // Get the block device handles by looking up directory contents.
66 const blkDirPath = "/sys/class/block"
67 blkDevs, err := os.ReadDir(blkDirPath)
68 if err != nil {
69 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
70 }
71 // Iterate over the handles, skipping any block device that either points to
72 // a partition, matches the exclusion list, or is smaller than minSize.
73 var suitable []string
74probeLoop:
75 for _, devInfo := range blkDevs {
76 // Skip devices according to the exclusion list.
77 for _, prefix := range exclude {
78 if strings.HasPrefix(devInfo.Name(), prefix) {
79 continue probeLoop
80 }
81 }
82
83 // Skip partition symlinks.
84 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
85 continue
86 } else if !os.IsNotExist(err) {
87 return nil, fmt.Errorf("while probing sysfs: %w", err)
88 }
89
90 // Skip devices of insufficient size.
91 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brunad131882023-06-28 16:42:20 +020092 dev, err := blockdev.Open(devPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +020093 if err != nil {
94 return nil, fmt.Errorf("couldn't open a block device at %q: %w", devPath, err)
95 }
Lorenz Brunad131882023-06-28 16:42:20 +020096 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +020097 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +020098 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +020099 continue
100 }
101
102 suitable = append(suitable, devInfo.Name())
103 }
104 return suitable, nil
105}
106
Mateusz Zalega43e21072021-10-08 18:05:29 +0200107func main() {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200108 bringup.Runnable(installerRunnable).Run()
109}
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100110
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200111func installerRunnable(ctx context.Context) error {
112 l := supervisor.Logger(ctx)
113
114 l.Info("Metropolis Installer")
115 l.Info("Copyright (c) 2024 The Monogon Project Authors")
116 l.Info("")
117
118 // Validate we are running via EFI.
119 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200120 // nolint:ST1005
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200121 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200122 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100123
Mateusz Zalega43e21072021-10-08 18:05:29 +0200124 // Read the installer ESP UUID from efivarfs.
125 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
126 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200127 return fmt.Errorf("while reading the installer ESP UUID: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200128 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100129 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
130 var espDev string
131 var retries = 30
132 for {
133 // Look up the installer partition based on espUuid.
134 espDev, err = sysfs.DeviceByPartUUID(espUuid)
135 if err == nil {
136 break
137 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
138 time.Sleep(1 * time.Second)
139 retries--
140 } else {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200141 return fmt.Errorf("while resolving the installer device handle: %w", err)
Lorenz Brun57d06a72022-01-13 14:12:27 +0100142 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200143 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100144 espPath := filepath.Join("/dev", espDev)
Jan Schär5fdca562025-04-14 11:33:29 +0000145 // Mount the installer partition. The OS image will be read from it.
Mateusz Zalega43e21072021-10-08 18:05:29 +0200146 if err := mountInstallerESP(espPath); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200147 return fmt.Errorf("while mounting the installer ESP: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200148 }
149
Jan Schärc1b6df42025-03-20 08:52:18 +0000150 nodeParameters, err := structfs.OSPathBlob("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100151 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200152 return fmt.Errorf("failed to open node parameters from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100153 }
154
Jan Schär5fdca562025-04-14 11:33:29 +0000155 ociImage, err := oci.ReadLayout("/installer/metropolis-installer/osimage")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100156 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000157 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100158 }
Jan Schär5fdca562025-04-14 11:33:29 +0000159 osImage, err := ociosimage.Read(ociImage)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100160 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000161 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100162 }
Jan Schär5fdca562025-04-14 11:33:29 +0000163
164 efiPayload, err := osImage.Payload("kernel.efi")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100165 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000166 return fmt.Errorf("cannot open EFI payload in OS image: %w", err)
167 }
168 systemImage, err := osImage.Payload("system")
169 if err != nil {
170 return fmt.Errorf("cannot open system image in OS image: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100171 }
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100172
Mateusz Zalega43e21072021-10-08 18:05:29 +0200173 // Build the osimage parameters.
174 installParams := osimage.Params{
175 PartitionSize: osimage.PartitionSizeInfo{
176 // ESP is the size of the node ESP partition, expressed in mebibytes.
Lorenz Brun35fcf032023-06-29 04:15:58 +0200177 ESP: 384,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200178 // System is the size of the node system partition, expressed in
179 // mebibytes.
180 System: 4096,
181 // Data must be nonzero in order for the data partition to be created.
182 // osimage will extend the data partition to fill all the available space
183 // whenever it's writing to block devices, such as now.
184 Data: 128,
185 },
Lorenz Brunad131882023-06-28 16:42:20 +0200186 SystemImage: systemImage,
Jan Schärc1b6df42025-03-20 08:52:18 +0000187 EFIPayload: efiPayload,
188 ABLoader: structfs.Bytes(abloader),
189 NodeParameters: nodeParameters,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200190 }
191 // Calculate the minimum target size based on the installation parameters.
192 minSize := uint64((installParams.PartitionSize.ESP +
Jan Schär42ef7c72024-03-18 15:09:51 +0100193 installParams.PartitionSize.System*2 +
Mateusz Zalega43e21072021-10-08 18:05:29 +0200194 installParams.PartitionSize.Data + 1) * mib)
195
196 // Look for suitable block devices, given the minimum size.
197 blkDevs, err := findInstallableBlockDevices(espDev, minSize)
198 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200199 return err
Mateusz Zalega43e21072021-10-08 18:05:29 +0200200 }
201 if len(blkDevs) == 0 {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200202 return fmt.Errorf("couldn't find a suitable block device")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200203 }
204 // Set the first suitable block device found as the installation target.
205 tgtBlkdevName := blkDevs[0]
206 // Update the osimage parameters with a path pointing at the target device.
207 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200208
209 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
210 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200211 return fmt.Errorf("error opening target device: %w", err)
Lorenz Brunad131882023-06-28 16:42:20 +0200212 }
213 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200214
215 // Use osimage to partition the target block device and set up its ESP.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200216 // Write will return an EFI boot entry on success.
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200217 l.Infof("Installing to %s...", tgtBlkdevPath)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200218 be, err := osimage.Write(&installParams)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200219 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200220 return fmt.Errorf("while installing: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200221 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200222
223 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200224 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200225 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200226 return fmt.Errorf("while creating a boot entry: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200227 }
228 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200229 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200230 return fmt.Errorf("while adjusting the boot order: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200231 }
232
233 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200234 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200235 unix.Sync()
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200236 l.Info("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200237 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200238 return nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200239}