blob: 0863808976ddbdcff5793e08f90b30815d756ad7 [file] [log] [blame]
Mateusz Zalega43e21072021-10-08 18:05:29 +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// Installer creates a Metropolis image at a suitable block device based on the
18// installer bundle present in the installation medium's ESP, after which it
19// reboots. It's meant to be used as an init process.
20package main
21
22import (
Lorenz Brun0b93c8d2021-11-09 03:58:40 +010023 "archive/zip"
Lorenz Brun54a5a052023-10-02 16:40:11 +020024 "bytes"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020025 "context"
Lorenz Brun54a5a052023-10-02 16:40:11 +020026 _ "embed"
Lorenz Brun57d06a72022-01-13 14:12:27 +010027 "errors"
Mateusz Zalega43e21072021-10-08 18:05:29 +020028 "fmt"
Lorenz Brunad131882023-06-28 16:42:20 +020029 "io/fs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020030 "os"
31 "path/filepath"
32 "strings"
Lorenz Brun57d06a72022-01-13 14:12:27 +010033 "time"
Mateusz Zalega43e21072021-10-08 18:05:29 +020034
35 "golang.org/x/sys/unix"
Serge Bazanski97783222021-12-14 16:04:26 +010036
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020037 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020038 "source.monogon.dev/osbase/bringup"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020039 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020040 "source.monogon.dev/osbase/efivarfs"
Tim Windelschmidt96e014e2024-09-10 02:26:13 +020041 "source.monogon.dev/osbase/supervisor"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020042 "source.monogon.dev/osbase/sysfs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020043)
44
Lorenz Brun54a5a052023-10-02 16:40:11 +020045//go:embed metropolis/node/core/abloader/abloader_bin.efi
46var abloader []byte
47
Mateusz Zalega43e21072021-10-08 18:05:29 +020048const mib = 1024 * 1024
49
Mateusz Zalega43e21072021-10-08 18:05:29 +020050// mountInstallerESP mounts the filesystem the installer was loaded from based
51// on espPath, which must point to the appropriate partition block device. The
52// filesystem is mounted at /installer.
53func mountInstallerESP(espPath string) error {
54 // Create the mountpoint.
55 if err := unix.Mkdir("/installer", 0700); err != nil {
56 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
57 }
58 // Mount the filesystem.
59 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
60 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
61 }
62 return nil
63}
64
65// findInstallableBlockDevices returns names of all the block devices suitable
66// for hosting a Metropolis installation, limited by the size expressed in
67// bytes minSize. The install medium espDev will be excluded from the result.
68func findInstallableBlockDevices(espDev string, minSize uint64) ([]string, error) {
69 // Use the partition's name to find and return the name of its parent
70 // device. It will be excluded from the list of suitable target devices.
71 srcDev, err := sysfs.ParentBlockDevice(espDev)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +020072 if err != nil {
Tim Windelschmidt096654a2024-04-18 23:10:19 +020073 return nil, fmt.Errorf("failed to fetch parent device: %w", err)
74 }
Mateusz Zalega43e21072021-10-08 18:05:29 +020075 // Build the exclusion list containing forbidden handle prefixes.
76 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
77
78 // Get the block device handles by looking up directory contents.
79 const blkDirPath = "/sys/class/block"
80 blkDevs, err := os.ReadDir(blkDirPath)
81 if err != nil {
82 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
83 }
84 // Iterate over the handles, skipping any block device that either points to
85 // a partition, matches the exclusion list, or is smaller than minSize.
86 var suitable []string
87probeLoop:
88 for _, devInfo := range blkDevs {
89 // Skip devices according to the exclusion list.
90 for _, prefix := range exclude {
91 if strings.HasPrefix(devInfo.Name(), prefix) {
92 continue probeLoop
93 }
94 }
95
96 // Skip partition symlinks.
97 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
98 continue
99 } else if !os.IsNotExist(err) {
100 return nil, fmt.Errorf("while probing sysfs: %w", err)
101 }
102
103 // Skip devices of insufficient size.
104 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brunad131882023-06-28 16:42:20 +0200105 dev, err := blockdev.Open(devPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200106 if err != nil {
107 return nil, fmt.Errorf("couldn't open a block device at %q: %w", devPath, err)
108 }
Lorenz Brunad131882023-06-28 16:42:20 +0200109 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200110 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +0200111 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +0200112 continue
113 }
114
115 suitable = append(suitable, devInfo.Name())
116 }
117 return suitable, nil
118}
119
Lorenz Brunad131882023-06-28 16:42:20 +0200120// FileSizedReader is a small adapter from fs.File to fs.SizedReader
121// Panics on Stat() failure, so should only be used with sources where Stat()
122// cannot fail.
123type FileSizedReader struct {
124 fs.File
Mateusz Zalega43e21072021-10-08 18:05:29 +0200125}
126
Lorenz Brunad131882023-06-28 16:42:20 +0200127func (f FileSizedReader) Size() int64 {
128 stat, err := f.Stat()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200129 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200130 panic(err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200131 }
Lorenz Brunad131882023-06-28 16:42:20 +0200132 return stat.Size()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200133}
134
135func main() {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200136 bringup.Runnable(installerRunnable).Run()
137}
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100138
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200139func installerRunnable(ctx context.Context) error {
140 l := supervisor.Logger(ctx)
141
142 l.Info("Metropolis Installer")
143 l.Info("Copyright (c) 2024 The Monogon Project Authors")
144 l.Info("")
145
146 // Validate we are running via EFI.
147 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
148 //nolint:ST1005
149 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200150 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100151
Mateusz Zalega43e21072021-10-08 18:05:29 +0200152 // Read the installer ESP UUID from efivarfs.
153 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
154 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200155 return fmt.Errorf("while reading the installer ESP UUID: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200156 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100157 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
158 var espDev string
159 var retries = 30
160 for {
161 // Look up the installer partition based on espUuid.
162 espDev, err = sysfs.DeviceByPartUUID(espUuid)
163 if err == nil {
164 break
165 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
166 time.Sleep(1 * time.Second)
167 retries--
168 } else {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200169 return fmt.Errorf("while resolving the installer device handle: %w", err)
Lorenz Brun57d06a72022-01-13 14:12:27 +0100170 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200171 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100172 espPath := filepath.Join("/dev", espDev)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200173 // Mount the installer partition. The installer bundle will be read from it.
174 if err := mountInstallerESP(espPath); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200175 return fmt.Errorf("while mounting the installer ESP: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200176 }
177
Lorenz Brun6c35e972021-12-14 03:08:23 +0100178 nodeParameters, err := os.Open("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100179 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200180 return fmt.Errorf("failed to open node parameters from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100181 }
182
183 // TODO(lorenz): Replace with proper bundles
Lorenz Brun6c35e972021-12-14 03:08:23 +0100184 bundle, err := zip.OpenReader("/installer/metropolis-installer/bundle.bin")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100185 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200186 return fmt.Errorf("failed to open node bundle from ESP: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100187 }
188 defer bundle.Close()
189 efiPayload, err := bundle.Open("kernel_efi.efi")
190 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200191 return fmt.Errorf("cannot open EFI payload in bundle: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100192 }
193 defer efiPayload.Close()
Mateusz Zalega8c2c7712022-01-25 19:42:21 +0100194 systemImage, err := bundle.Open("verity_rootfs.img")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100195 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200196 return fmt.Errorf("cannot open system image in bundle: %w", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100197 }
198 defer systemImage.Close()
199
Mateusz Zalega43e21072021-10-08 18:05:29 +0200200 // Build the osimage parameters.
201 installParams := osimage.Params{
202 PartitionSize: osimage.PartitionSizeInfo{
203 // ESP is the size of the node ESP partition, expressed in mebibytes.
Lorenz Brun35fcf032023-06-29 04:15:58 +0200204 ESP: 384,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200205 // System is the size of the node system partition, expressed in
206 // mebibytes.
207 System: 4096,
208 // Data must be nonzero in order for the data partition to be created.
209 // osimage will extend the data partition to fill all the available space
210 // whenever it's writing to block devices, such as now.
211 Data: 128,
212 },
Lorenz Brunad131882023-06-28 16:42:20 +0200213 SystemImage: systemImage,
214 EFIPayload: FileSizedReader{efiPayload},
Lorenz Brun54a5a052023-10-02 16:40:11 +0200215 ABLoader: bytes.NewReader(abloader),
Lorenz Brunad131882023-06-28 16:42:20 +0200216 NodeParameters: FileSizedReader{nodeParameters},
Mateusz Zalega43e21072021-10-08 18:05:29 +0200217 }
218 // Calculate the minimum target size based on the installation parameters.
219 minSize := uint64((installParams.PartitionSize.ESP +
Jan Schär42ef7c72024-03-18 15:09:51 +0100220 installParams.PartitionSize.System*2 +
Mateusz Zalega43e21072021-10-08 18:05:29 +0200221 installParams.PartitionSize.Data + 1) * mib)
222
223 // Look for suitable block devices, given the minimum size.
224 blkDevs, err := findInstallableBlockDevices(espDev, minSize)
225 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200226 return fmt.Errorf(err.Error())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200227 }
228 if len(blkDevs) == 0 {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200229 return fmt.Errorf("couldn't find a suitable block device")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200230 }
231 // Set the first suitable block device found as the installation target.
232 tgtBlkdevName := blkDevs[0]
233 // Update the osimage parameters with a path pointing at the target device.
234 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200235
236 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
237 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200238 return fmt.Errorf("error opening target device: %w", err)
Lorenz Brunad131882023-06-28 16:42:20 +0200239 }
240 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200241
242 // Use osimage to partition the target block device and set up its ESP.
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200243 // Write will return an EFI boot entry on success.
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200244 l.Infof("Installing to %s...", tgtBlkdevPath)
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200245 be, err := osimage.Write(&installParams)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200246 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200247 return fmt.Errorf("while installing: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200248 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200249
250 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200251 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200252 if err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200253 return fmt.Errorf("while creating a boot entry: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200254 }
255 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200256 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200257 return fmt.Errorf("while adjusting the boot order: %w", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200258 }
259
260 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200261 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200262 unix.Sync()
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200263 l.Info("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200264 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
Tim Windelschmidt96e014e2024-09-10 02:26:13 +0200265 return nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200266}