blob: 930c4725f81bb5c86a0697bf2ca4ba70f1198566 [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
Lorenz Brun3545df32025-07-07 11:15:15 +020021 "source.monogon.dev/go/logging"
Jan Schäre19d2792025-06-23 12:37:58 +000022 "source.monogon.dev/metropolis/installer/install"
Lorenz Brun3545df32025-07-07 11:15:15 +020023 "source.monogon.dev/metropolis/node/core/devmgr"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020024 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020025 "source.monogon.dev/osbase/bringup"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020026 "source.monogon.dev/osbase/efivarfs"
Jan Schär5fdca562025-04-14 11:33:29 +000027 "source.monogon.dev/osbase/oci"
Jan Schäre19d2792025-06-23 12:37:58 +000028 "source.monogon.dev/osbase/oci/osimage"
Jan Schärc1b6df42025-03-20 08:52:18 +000029 "source.monogon.dev/osbase/structfs"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020030 "source.monogon.dev/osbase/supervisor"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020031 "source.monogon.dev/osbase/sysfs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020032)
33
Jan Schär69b76872025-05-14 16:39:47 +000034//go:embed metropolis/node/abloader/abloader_bin.efi
Lorenz Brun54a5a052023-10-02 16:40:11 +020035var abloader []byte
36
Jan Schär36f3b6d2025-05-20 09:05:12 +000037//go:embed build/copyright_line.txt
Jan Schär10670e52025-04-23 12:54:48 +000038var copyrightLine string
39
Mateusz Zalega43e21072021-10-08 18:05:29 +020040const mib = 1024 * 1024
41
Mateusz Zalega43e21072021-10-08 18:05:29 +020042// mountInstallerESP mounts the filesystem the installer was loaded from based
43// on espPath, which must point to the appropriate partition block device. The
44// filesystem is mounted at /installer.
45func mountInstallerESP(espPath string) error {
46 // Create the mountpoint.
47 if err := unix.Mkdir("/installer", 0700); err != nil {
48 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
49 }
50 // Mount the filesystem.
51 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
52 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
53 }
54 return nil
55}
56
57// findInstallableBlockDevices returns names of all the block devices suitable
58// for hosting a Metropolis installation, limited by the size expressed in
59// bytes minSize. The install medium espDev will be excluded from the result.
Lorenz Brun3545df32025-07-07 11:15:15 +020060func findInstallableBlockDevices(l logging.Leveled, espDev string, minSize uint64) ([]string, error) {
Mateusz Zalega43e21072021-10-08 18:05:29 +020061 // Use the partition's name to find and return the name of its parent
62 // device. It will be excluded from the list of suitable target devices.
63 srcDev, err := sysfs.ParentBlockDevice(espDev)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020064 if err != nil {
Tim Windelschmidt096654a2024-04-18 23:10:19 +020065 return nil, fmt.Errorf("failed to fetch parent device: %w", err)
66 }
Mateusz Zalega43e21072021-10-08 18:05:29 +020067 // Build the exclusion list containing forbidden handle prefixes.
68 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
69
70 // Get the block device handles by looking up directory contents.
71 const blkDirPath = "/sys/class/block"
72 blkDevs, err := os.ReadDir(blkDirPath)
73 if err != nil {
74 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
75 }
76 // Iterate over the handles, skipping any block device that either points to
77 // a partition, matches the exclusion list, or is smaller than minSize.
78 var suitable []string
79probeLoop:
80 for _, devInfo := range blkDevs {
81 // Skip devices according to the exclusion list.
82 for _, prefix := range exclude {
83 if strings.HasPrefix(devInfo.Name(), prefix) {
84 continue probeLoop
85 }
86 }
87
88 // Skip partition symlinks.
89 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
90 continue
91 } else if !os.IsNotExist(err) {
92 return nil, fmt.Errorf("while probing sysfs: %w", err)
93 }
94
95 // Skip devices of insufficient size.
96 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brun3545df32025-07-07 11:15:15 +020097 dev, err := blockdev.Open(devPath, blockdev.WithReadonly)
Mateusz Zalega43e21072021-10-08 18:05:29 +020098 if err != nil {
Lorenz Brun3545df32025-07-07 11:15:15 +020099 l.Warningf("couldn't open a block device at %q, ignoring: %v", devPath, err)
100 continue
Mateusz Zalega43e21072021-10-08 18:05:29 +0200101 }
Lorenz Brunad131882023-06-28 16:42:20 +0200102 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200103 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +0200104 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +0200105 continue
106 }
107
108 suitable = append(suitable, devInfo.Name())
109 }
110 return suitable, nil
111}
112
Mateusz Zalega43e21072021-10-08 18:05:29 +0200113func main() {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200114 bringup.Runnable(installerRunnable).Run()
115}
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100116
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200117func installerRunnable(ctx context.Context) error {
118 l := supervisor.Logger(ctx)
119
120 l.Info("Metropolis Installer")
Jan Schär10670e52025-04-23 12:54:48 +0000121 l.Info(copyrightLine)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200122 l.Info("")
123
Lorenz Brun3545df32025-07-07 11:15:15 +0200124 devmgrSvc := devmgr.New()
125 supervisor.Run(ctx, "devmgr", devmgrSvc.Run)
126
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200127 // Validate we are running via EFI.
128 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200129 // nolint:ST1005
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200130 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200131 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100132
Mateusz Zalega43e21072021-10-08 18:05:29 +0200133 // Read the installer ESP UUID from efivarfs.
134 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
135 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200136 return fmt.Errorf("while reading the installer ESP UUID: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200137 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100138 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
139 var espDev string
140 var retries = 30
141 for {
142 // Look up the installer partition based on espUuid.
143 espDev, err = sysfs.DeviceByPartUUID(espUuid)
144 if err == nil {
145 break
146 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
147 time.Sleep(1 * time.Second)
148 retries--
149 } else {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200150 return fmt.Errorf("while resolving the installer device handle: %w", err)
Lorenz Brun57d06a72022-01-13 14:12:27 +0100151 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200152 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100153 espPath := filepath.Join("/dev", espDev)
Jan Schär5fdca562025-04-14 11:33:29 +0000154 // Mount the installer partition. The OS image will be read from it.
Mateusz Zalega43e21072021-10-08 18:05:29 +0200155 if err := mountInstallerESP(espPath); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200156 return fmt.Errorf("while mounting the installer ESP: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200157 }
158
Jan Schärc1b6df42025-03-20 08:52:18 +0000159 nodeParameters, err := structfs.OSPathBlob("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100160 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200161 return fmt.Errorf("failed to open node parameters from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100162 }
163
Jan Schär5fdca562025-04-14 11:33:29 +0000164 ociImage, err := oci.ReadLayout("/installer/metropolis-installer/osimage")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100165 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000166 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100167 }
Jan Schäre19d2792025-06-23 12:37:58 +0000168 osImage, err := osimage.Read(ociImage)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100169 if err != nil {
Jan Schär5fdca562025-04-14 11:33:29 +0000170 return fmt.Errorf("failed to read OS image from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100171 }
Jan Schär5fdca562025-04-14 11:33:29 +0000172
Jan Schäre19d2792025-06-23 12:37:58 +0000173 // Build the install parameters.
174 installParams := install.Params{
175 PartitionSize: install.PartitionSizeInfo{
Mateusz Zalega43e21072021-10-08 18:05:29 +0200176 // 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.
Jan Schäre19d2792025-06-23 12:37:58 +0000182 // install will extend the data partition to fill all the available space
Mateusz Zalega43e21072021-10-08 18:05:29 +0200183 // whenever it's writing to block devices, such as now.
184 Data: 128,
185 },
Jan Schärdaf9e952025-06-23 13:28:16 +0000186 OSImage: osImage,
Jan Schärc1b6df42025-03-20 08:52:18 +0000187 ABLoader: structfs.Bytes(abloader),
188 NodeParameters: nodeParameters,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200189 }
190 // Calculate the minimum target size based on the installation parameters.
191 minSize := uint64((installParams.PartitionSize.ESP +
Jan Schär42ef7c72024-03-18 15:09:51 +0100192 installParams.PartitionSize.System*2 +
Mateusz Zalega43e21072021-10-08 18:05:29 +0200193 installParams.PartitionSize.Data + 1) * mib)
194
195 // Look for suitable block devices, given the minimum size.
Lorenz Brun3545df32025-07-07 11:15:15 +0200196 blkDevs, err := findInstallableBlockDevices(l, espDev, minSize)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200197 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200198 return err
Mateusz Zalega43e21072021-10-08 18:05:29 +0200199 }
200 if len(blkDevs) == 0 {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200201 return fmt.Errorf("couldn't find a suitable block device")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200202 }
203 // Set the first suitable block device found as the installation target.
204 tgtBlkdevName := blkDevs[0]
Jan Schäre19d2792025-06-23 12:37:58 +0000205 // Update the install parameters with a path pointing at the target device.
Mateusz Zalega43e21072021-10-08 18:05:29 +0200206 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200207
208 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
209 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200210 return fmt.Errorf("error opening target device: %w", err)
Lorenz Brunad131882023-06-28 16:42:20 +0200211 }
212 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200213
Jan Schäre19d2792025-06-23 12:37:58 +0000214 // Use install to partition the target block device and set up its ESP.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200215 // Write will return an EFI boot entry on success.
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200216 l.Infof("Installing to %s...", tgtBlkdevPath)
Jan Schäre19d2792025-06-23 12:37:58 +0000217 be, err := install.Write(&installParams)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200218 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200219 return fmt.Errorf("while installing: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200220 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200221
222 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200223 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200224 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200225 return fmt.Errorf("while creating a boot entry: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200226 }
227 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200228 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200229 return fmt.Errorf("while adjusting the boot order: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200230 }
231
232 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200233 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200234 unix.Sync()
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200235 l.Info("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200236 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200237 return nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200238}