blob: 8aa8836719c83bcb57cd3e20e7cf44d96ead5f7d [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"
25 _ "embed"
Lorenz Brun57d06a72022-01-13 14:12:27 +010026 "errors"
Mateusz Zalega43e21072021-10-08 18:05:29 +020027 "fmt"
Lorenz Brunad131882023-06-28 16:42:20 +020028 "io/fs"
Mateusz Zalega43e21072021-10-08 18:05:29 +020029 "os"
30 "path/filepath"
31 "strings"
32 "syscall"
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
Mateusz Zalega43e21072021-10-08 18:05:29 +020037 "source.monogon.dev/metropolis/node/build/mkimage/osimage"
Lorenz Brunad131882023-06-28 16:42:20 +020038 "source.monogon.dev/metropolis/pkg/blockdev"
Mateusz Zalega43e21072021-10-08 18:05:29 +020039 "source.monogon.dev/metropolis/pkg/efivarfs"
40 "source.monogon.dev/metropolis/pkg/sysfs"
41)
42
Lorenz Brun54a5a052023-10-02 16:40:11 +020043//go:embed metropolis/node/core/abloader/abloader_bin.efi
44var abloader []byte
45
Mateusz Zalega43e21072021-10-08 18:05:29 +020046const mib = 1024 * 1024
47
48// mountPseudoFS mounts efivarfs, devtmpfs and sysfs, used by the installer in
49// the block device discovery stage.
50func mountPseudoFS() error {
51 for _, m := range []struct {
52 dir string
53 fs string
54 flags uintptr
55 }{
56 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
57 {efivarfs.Path, "efivarfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
58 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
59 } {
60 if err := unix.Mkdir(m.dir, 0700); err != nil && !os.IsExist(err) {
61 return fmt.Errorf("couldn't create the mountpoint at %q: %w", m.dir, err)
62 }
63 if err := unix.Mount(m.fs, m.dir, m.fs, m.flags, ""); err != nil {
64 return fmt.Errorf("couldn't mount %q at %q: %w", m.fs, m.dir, err)
65 }
66 }
67 return nil
68}
69
70// mountInstallerESP mounts the filesystem the installer was loaded from based
71// on espPath, which must point to the appropriate partition block device. The
72// filesystem is mounted at /installer.
73func mountInstallerESP(espPath string) error {
74 // Create the mountpoint.
75 if err := unix.Mkdir("/installer", 0700); err != nil {
76 return fmt.Errorf("couldn't create the installer mountpoint: %w", err)
77 }
78 // Mount the filesystem.
79 if err := unix.Mount(espPath, "/installer", "vfat", unix.MS_NOEXEC|unix.MS_RDONLY, ""); err != nil {
80 return fmt.Errorf("couldn't mount the installer ESP (%q -> %q): %w", espPath, "/installer", err)
81 }
82 return nil
83}
84
85// findInstallableBlockDevices returns names of all the block devices suitable
86// for hosting a Metropolis installation, limited by the size expressed in
87// bytes minSize. The install medium espDev will be excluded from the result.
88func findInstallableBlockDevices(espDev string, minSize uint64) ([]string, error) {
89 // Use the partition's name to find and return the name of its parent
90 // device. It will be excluded from the list of suitable target devices.
91 srcDev, err := sysfs.ParentBlockDevice(espDev)
92 // Build the exclusion list containing forbidden handle prefixes.
93 exclude := []string{"dm-", "zram", "ram", "loop", srcDev}
94
95 // Get the block device handles by looking up directory contents.
96 const blkDirPath = "/sys/class/block"
97 blkDevs, err := os.ReadDir(blkDirPath)
98 if err != nil {
99 return nil, fmt.Errorf("couldn't read %q: %w", blkDirPath, err)
100 }
101 // Iterate over the handles, skipping any block device that either points to
102 // a partition, matches the exclusion list, or is smaller than minSize.
103 var suitable []string
104probeLoop:
105 for _, devInfo := range blkDevs {
106 // Skip devices according to the exclusion list.
107 for _, prefix := range exclude {
108 if strings.HasPrefix(devInfo.Name(), prefix) {
109 continue probeLoop
110 }
111 }
112
113 // Skip partition symlinks.
114 if _, err := os.Stat(filepath.Join(blkDirPath, devInfo.Name(), "partition")); err == nil {
115 continue
116 } else if !os.IsNotExist(err) {
117 return nil, fmt.Errorf("while probing sysfs: %w", err)
118 }
119
120 // Skip devices of insufficient size.
121 devPath := filepath.Join("/dev", devInfo.Name())
Lorenz Brunad131882023-06-28 16:42:20 +0200122 dev, err := blockdev.Open(devPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200123 if err != nil {
124 return nil, fmt.Errorf("couldn't open a block device at %q: %w", devPath, err)
125 }
Lorenz Brunad131882023-06-28 16:42:20 +0200126 devSize := uint64(dev.BlockCount() * dev.BlockSize())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200127 dev.Close()
Lorenz Brunad131882023-06-28 16:42:20 +0200128 if devSize < minSize {
Mateusz Zalega43e21072021-10-08 18:05:29 +0200129 continue
130 }
131
132 suitable = append(suitable, devInfo.Name())
133 }
134 return suitable, nil
135}
136
Lorenz Brunad131882023-06-28 16:42:20 +0200137// FileSizedReader is a small adapter from fs.File to fs.SizedReader
138// Panics on Stat() failure, so should only be used with sources where Stat()
139// cannot fail.
140type FileSizedReader struct {
141 fs.File
Mateusz Zalega43e21072021-10-08 18:05:29 +0200142}
143
Lorenz Brunad131882023-06-28 16:42:20 +0200144func (f FileSizedReader) Size() int64 {
145 stat, err := f.Stat()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200146 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200147 panic(err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200148 }
Lorenz Brunad131882023-06-28 16:42:20 +0200149 return stat.Size()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200150}
151
152func main() {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100153 // Reboot on panic after a delay. The error string will have been printed
154 // before recover is called.
155 defer func() {
156 if r := recover(); r != nil {
Serge Bazanskif71fe922023-03-22 01:10:37 +0100157 logf("Fatal error: %v", r)
158 logf("The installation could not be finalized. Please reboot to continue.")
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100159 syscall.Pause()
160 }
161 }()
162
Mateusz Zalega43e21072021-10-08 18:05:29 +0200163 // Mount sysfs, devtmpfs and efivarfs.
164 if err := mountPseudoFS(); err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100165 panicf("While mounting pseudo-filesystems: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200166 }
Serge Bazanskif71fe922023-03-22 01:10:37 +0100167
168 go logPiper()
169 logf("Metropolis Installer")
170 logf("Copyright (c) 2023 The Monogon Project Authors")
171 logf("")
172
Mateusz Zalega43e21072021-10-08 18:05:29 +0200173 // Read the installer ESP UUID from efivarfs.
174 espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
175 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100176 panicf("While reading the installer ESP UUID: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200177 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100178 // Wait for up to 30 tries @ 1s (30s) for the ESP to show up
179 var espDev string
180 var retries = 30
181 for {
182 // Look up the installer partition based on espUuid.
183 espDev, err = sysfs.DeviceByPartUUID(espUuid)
184 if err == nil {
185 break
186 } else if errors.Is(err, sysfs.ErrDevNotFound) && retries > 0 {
187 time.Sleep(1 * time.Second)
188 retries--
189 } else {
190 panicf("While resolving the installer device handle: %v", err)
191 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200192 }
Lorenz Brun57d06a72022-01-13 14:12:27 +0100193 espPath := filepath.Join("/dev", espDev)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200194 // Mount the installer partition. The installer bundle will be read from it.
195 if err := mountInstallerESP(espPath); err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100196 panicf("While mounting the installer ESP: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200197 }
198
Lorenz Brun6c35e972021-12-14 03:08:23 +0100199 nodeParameters, err := os.Open("/installer/metropolis-installer/nodeparams.pb")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100200 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100201 panicf("Failed to open node parameters from ESP: %v", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100202 }
203
204 // TODO(lorenz): Replace with proper bundles
Lorenz Brun6c35e972021-12-14 03:08:23 +0100205 bundle, err := zip.OpenReader("/installer/metropolis-installer/bundle.bin")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100206 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100207 panicf("Failed to open node bundle from ESP: %v", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100208 }
209 defer bundle.Close()
210 efiPayload, err := bundle.Open("kernel_efi.efi")
211 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100212 panicf("Cannot open EFI payload in bundle: %v", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100213 }
214 defer efiPayload.Close()
Mateusz Zalega8c2c7712022-01-25 19:42:21 +0100215 systemImage, err := bundle.Open("verity_rootfs.img")
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100216 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100217 panicf("Cannot open system image in bundle: %v", err)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100218 }
219 defer systemImage.Close()
220
Mateusz Zalega43e21072021-10-08 18:05:29 +0200221 // Build the osimage parameters.
222 installParams := osimage.Params{
223 PartitionSize: osimage.PartitionSizeInfo{
224 // ESP is the size of the node ESP partition, expressed in mebibytes.
Lorenz Brun35fcf032023-06-29 04:15:58 +0200225 ESP: 384,
Mateusz Zalega43e21072021-10-08 18:05:29 +0200226 // System is the size of the node system partition, expressed in
227 // mebibytes.
228 System: 4096,
229 // Data must be nonzero in order for the data partition to be created.
230 // osimage will extend the data partition to fill all the available space
231 // whenever it's writing to block devices, such as now.
232 Data: 128,
233 },
Lorenz Brunad131882023-06-28 16:42:20 +0200234 SystemImage: systemImage,
235 EFIPayload: FileSizedReader{efiPayload},
Lorenz Brun54a5a052023-10-02 16:40:11 +0200236 ABLoader: bytes.NewReader(abloader),
Lorenz Brunad131882023-06-28 16:42:20 +0200237 NodeParameters: FileSizedReader{nodeParameters},
Mateusz Zalega43e21072021-10-08 18:05:29 +0200238 }
239 // Calculate the minimum target size based on the installation parameters.
240 minSize := uint64((installParams.PartitionSize.ESP +
241 installParams.PartitionSize.System +
242 installParams.PartitionSize.Data + 1) * mib)
243
244 // Look for suitable block devices, given the minimum size.
245 blkDevs, err := findInstallableBlockDevices(espDev, minSize)
246 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100247 panicf(err.Error())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200248 }
249 if len(blkDevs) == 0 {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100250 panicf("Couldn't find a suitable block device.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200251 }
252 // Set the first suitable block device found as the installation target.
253 tgtBlkdevName := blkDevs[0]
254 // Update the osimage parameters with a path pointing at the target device.
255 tgtBlkdevPath := filepath.Join("/dev", tgtBlkdevName)
Lorenz Brunad131882023-06-28 16:42:20 +0200256
257 tgtBlockDev, err := blockdev.Open(tgtBlkdevPath)
258 if err != nil {
259 panicf("error opening target device: %v", err)
260 }
261 installParams.Output = tgtBlockDev
Mateusz Zalega43e21072021-10-08 18:05:29 +0200262
263 // Use osimage to partition the target block device and set up its ESP.
264 // Create will return an EFI boot entry on success.
Serge Bazanskif71fe922023-03-22 01:10:37 +0100265 logf("Installing to %s...", tgtBlkdevPath)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200266 be, err := osimage.Create(&installParams)
267 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100268 panicf("While installing: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200269 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200270
271 // Create an EFI boot entry for Metropolis.
Lorenz Brunca1cff02023-06-26 17:52:44 +0200272 en, err := efivarfs.AddBootEntry(be)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200273 if err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100274 panicf("While creating a boot entry: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200275 }
276 // Erase the preexisting boot order, leaving Metropolis as the only option.
Lorenz Brun9933ef02023-07-06 18:28:29 +0200277 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(en)}); err != nil {
Mateusz Zalegacdcc7392021-12-08 15:34:53 +0100278 panicf("While adjusting the boot order: %v", err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200279 }
280
281 // Reboot.
Lorenz Brunad131882023-06-28 16:42:20 +0200282 tgtBlockDev.Close()
Mateusz Zalega43e21072021-10-08 18:05:29 +0200283 unix.Sync()
Serge Bazanskif71fe922023-03-22 01:10:37 +0100284 logf("Installation completed. Rebooting.")
Mateusz Zalega43e21072021-10-08 18:05:29 +0200285 unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
286}