blob: 0f2f40e21af5f1d20197956dc6da5f023d7c0ee1 [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// This package runs the installer image in a VM provided with an empty block
18// device. It then examines the installer console output and the blok device to
19// determine whether the installation process completed without issue.
20package main
21
22import (
23 "bufio"
24 "bytes"
25 "fmt"
26 "io"
27 "log"
28 "os"
29 "os/exec"
Mateusz Zalega8cde8e72021-11-30 16:22:20 +010030 "path/filepath"
Mateusz Zalega43e21072021-10-08 18:05:29 +020031 "syscall"
32 "testing"
33
Mateusz Zalega8cde8e72021-11-30 16:22:20 +010034 "github.com/bazelbuild/rules_go/go/tools/bazel"
Mateusz Zalega43e21072021-10-08 18:05:29 +020035 diskfs "github.com/diskfs/go-diskfs"
36 "github.com/diskfs/go-diskfs/disk"
37 "github.com/diskfs/go-diskfs/partition/gpt"
Lorenz Brun0b93c8d2021-11-09 03:58:40 +010038
Mateusz Zalega43e21072021-10-08 18:05:29 +020039 mctl "source.monogon.dev/metropolis/cli/metroctl/core"
Mateusz Zalega8cde8e72021-11-30 16:22:20 +010040 "source.monogon.dev/metropolis/node/build/mkimage/osimage"
41 "source.monogon.dev/metropolis/proto/api"
Mateusz Zalega43e21072021-10-08 18:05:29 +020042)
43
Mateusz Zalega8cde8e72021-11-30 16:22:20 +010044// Each variable in this block points to either a test dependency or a side
45// effect. These variables are initialized in TestMain using Bazel.
46var (
47 // installerEFIPayload is a filesystem path pointing at the unified kernel
48 // image dependency.
49 installerEFIPayload string
50 // testOSBundle is a filesystem path pointing at the Metropolis installation
51 // bundle.
52 testOSBundle string
53 // installerImage is a filesystem path pointing at the installer image that
54 // is generated during the test, and is removed afterwards.
55 installerImage string
56 // nodeStorage is a filesystem path pointing at the VM block device image
57 // Metropolis is installed to during the test. The file is removed afterwards.
58 nodeStorage string
Mateusz Zalega43e21072021-10-08 18:05:29 +020059)
60
61// runQemu starts a QEMU process and waits for it to finish. args is
62// concatenated to the list of predefined default arguments. It returns true if
63// expectedOutput is found in the serial port output. It may return an error.
64func runQemu(args []string, expectedOutput string) (bool, error) {
65 // Prepare the default parameter list.
66 defaultArgs := []string{
67 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults",
68 "-m", "512",
69 "-smp", "2",
70 "-cpu", "host",
71 "-drive", "if=pflash,format=raw,readonly,file=external/edk2/OVMF_CODE.fd",
72 "-drive", "if=pflash,format=raw,snapshot=on,file=external/edk2/OVMF_VARS.fd",
Mateusz Zalega43e21072021-10-08 18:05:29 +020073 "-serial", "stdio",
74 "-no-reboot",
75 }
76 // Join the parameter lists and prepare the Qemu command, but don't run it
77 // just yet.
78 qemuArgs := append(defaultArgs, args...)
79 qemuCmd := exec.Command("external/qemu/qemu-x86_64-softmmu", qemuArgs...)
80
81 // Copy the stdout and stderr output so that it could be matched against
82 // expectedOutput later.
83 var outBuf, errBuf bytes.Buffer
84 outWriter := bufio.NewWriter(&outBuf)
85 errWriter := bufio.NewWriter(&errBuf)
86 qemuCmd.Stdout = io.MultiWriter(os.Stdout, outWriter)
87 qemuCmd.Stderr = io.MultiWriter(os.Stderr, errWriter)
88 if err := qemuCmd.Run(); err != nil {
89 return false, fmt.Errorf("couldn't start QEMU: %w", err)
90 }
91 outWriter.Flush()
92 errWriter.Flush()
93
94 // Try matching against expectedOutput and return the result.
95 result := bytes.Contains(outBuf.Bytes(), []byte(expectedOutput)) ||
96 bytes.Contains(errBuf.Bytes(), []byte(expectedOutput))
97 return result, nil
98}
99
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100100// runQemuWithInstaller starts a QEMU process and waits for it to finish. args is
101// concatenated to the list of predefined default arguments. It returns true if
102// expectedOutput is found in the serial port output. It may return an error.
103func runQemuWithInstaller(args []string, expectedOutput string) (bool, error) {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100104 args = append(args, "-drive", "if=virtio,format=raw,snapshot=on,cache=unsafe,file="+installerImage)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100105 return runQemu(args, expectedOutput)
106}
107
Mateusz Zalega43e21072021-10-08 18:05:29 +0200108// getStorage creates a sparse file, given a size expressed in mebibytes, and
109// returns a path to that file. It may return an error.
110func getStorage(size int64) (string, error) {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100111 image, err := os.Create(nodeStorage)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200112 if err != nil {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100113 return "", fmt.Errorf("couldn't create the block device image at %q: %w", nodeStorage, err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200114 }
115 if err := syscall.Ftruncate(int(image.Fd()), size*1024*1024); err != nil {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100116 return "", fmt.Errorf("couldn't resize the block device image at %q: %w", nodeStorage, err)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200117 }
118 image.Close()
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100119 return nodeStorage, nil
Mateusz Zalega43e21072021-10-08 18:05:29 +0200120}
121
122// qemuDriveParam returns QEMU parameters required to run it with a
123// raw-format image at path.
124func qemuDriveParam(path string) []string {
125 return []string{"-drive", "if=virtio,format=raw,snapshot=off,cache=unsafe,file=" + path}
126}
127
128// checkEspContents verifies the presence of the EFI payload inside of image's
129// first partition. It returns nil on success.
130func checkEspContents(image *disk.Disk) error {
131 // Get the ESP.
132 fs, err := image.GetFilesystem(1)
133 if err != nil {
134 return fmt.Errorf("couldn't read the installer ESP: %w", err)
135 }
136 // Make sure the EFI payload exists by attempting to open it.
137 efiPayload, err := fs.OpenFile(osimage.EFIPayloadPath, os.O_RDONLY)
138 if err != nil {
139 return fmt.Errorf("couldn't open the installer's EFI Payload at %q: %w", osimage.EFIPayloadPath, err)
140 }
141 efiPayload.Close()
142 return nil
143}
144
145func TestMain(m *testing.M) {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100146 // Initialize global variables holding filesystem paths pointing to runtime
147 // dependencies and side effects.
148 paths := []struct {
149 // res is a pointer to the global variable initialized.
150 res *string
151 // dep states whether the path should be resolved as a dependency, rather
152 // than a side effect.
153 dep bool
154 // src is a source path, based on which res is resolved. In case of
155 // dependencies it must be a path relative to the repository root. For
156 // side effects, it must be just a filename.
157 src string
158 }{
159 {&installerEFIPayload, true, "metropolis/node/installer/kernel.efi"},
160 {&testOSBundle, true, "metropolis/test/installer/testos/testos_bundle.zip"},
161 {&installerImage, false, "installer.img"},
162 {&nodeStorage, false, "stor.img"},
163 }
164 for _, p := range paths {
165 if p.dep {
166 res, err := bazel.Runfile(p.src)
167 if err != nil {
168 log.Fatal(err)
169 }
170 *p.res = res
171 } else {
172 od := os.Getenv("TEST_TMPDIR")
173 // If od is empty, just use the working directory, which is set according
174 // to the rundir attribute of go_test.
175 *p.res = filepath.Join(od, p.src)
176 }
177 }
178
Mateusz Zalega43e21072021-10-08 18:05:29 +0200179 // Build the installer image with metroctl, given the EFI executable
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100180 // generated by Metropolis buildsystem.
181 installer, err := os.Open(installerEFIPayload)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200182 if err != nil {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100183 log.Fatalf("Couldn't open the installer EFI executable at %q: %s", installerEFIPayload, err.Error())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200184 }
185 info, err := installer.Stat()
186 if err != nil {
187 log.Fatalf("Couldn't stat the installer EFI executable: %s", err.Error())
188 }
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100189 bundle, err := os.Open(testOSBundle)
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100190 if err != nil {
191 log.Fatalf("failed to open TestOS bundle: %v", err)
192 }
193 bundleStat, err := bundle.Stat()
194 if err != nil {
195 log.Fatalf("failed to stat() TestOS bundle: %v", err)
196 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200197 iargs := mctl.MakeInstallerImageArgs{
198 Installer: installer,
199 InstallerSize: uint64(info.Size()),
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100200 TargetPath: installerImage,
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100201 NodeParams: &api.NodeParameters{},
202 Bundle: bundle,
203 BundleSize: uint64(bundleStat.Size()),
Mateusz Zalega43e21072021-10-08 18:05:29 +0200204 }
205 if err := mctl.MakeInstallerImage(iargs); err != nil {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100206 log.Fatalf("Couldn't create the installer image at %q: %s", installerImage, err.Error())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200207 }
208 // With common dependencies set up, run the tests.
209 code := m.Run()
210 // Clean up.
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100211 os.Remove(installerImage)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200212 os.Exit(code)
213}
214
215func TestInstallerImage(t *testing.T) {
216 // This test examines the installer image, making sure that the GPT and the
217 // ESP contents are in order.
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100218 image, err := diskfs.OpenWithMode(installerImage, diskfs.ReadOnly)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200219 if err != nil {
Mateusz Zalega8cde8e72021-11-30 16:22:20 +0100220 t.Errorf("Couldn't open the installer image at %q: %s", installerImage, err.Error())
Mateusz Zalega43e21072021-10-08 18:05:29 +0200221 }
222 // Verify that GPT exists.
223 ti, err := image.GetPartitionTable()
224 if ti.Type() != "gpt" {
225 t.Error("Couldn't verify that the installer image contains a GPT.")
226 }
227 // Check that the first partition is likely to be a valid ESP.
228 pi := ti.GetPartitions()
229 esp := (pi[0]).(*gpt.Partition)
230 if esp.Start == 0 || esp.End == 0 {
231 t.Error("The installer's ESP GPT entry looks off.")
232 }
233 // Verify that the image contains only one partition.
234 second := (pi[1]).(*gpt.Partition)
235 if second.Name != "" || second.Start != 0 || second.End != 0 {
236 t.Error("It appears the installer image contains more than one partition.")
237 }
238 // Verify the ESP contents.
239 if err := checkEspContents(image); err != nil {
240 t.Error(err.Error())
241 }
242}
243
244func TestNoBlockDevices(t *testing.T) {
245 // No block devices are passed to QEMU aside from the install medium. Expect
246 // the installer to fail at the device probe stage rather than attempting to
247 // use the medium as the target device.
248 expectedOutput := "couldn't find a suitable block device"
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100249 result, err := runQemuWithInstaller(nil, expectedOutput)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200250 if err != nil {
251 t.Error(err.Error())
252 }
253 if result != true {
254 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
255 }
256}
257
258func TestBlockDeviceTooSmall(t *testing.T) {
259 // Prepare the block device the installer will install to. This time the
260 // target device is too small to host a Metropolis installation.
261 imagePath, err := getStorage(64)
262 defer os.Remove(imagePath)
263 if err != nil {
264 t.Errorf(err.Error())
265 }
266
267 // Run QEMU. Expect the installer to fail with a predefined error string.
268 expectedOutput := "couldn't find a suitable block device"
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100269 result, err := runQemuWithInstaller(qemuDriveParam(imagePath), expectedOutput)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200270 if err != nil {
271 t.Error(err.Error())
272 }
273 if result != true {
274 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
275 }
276}
277
278func TestInstall(t *testing.T) {
279 // Prepare the block device image the installer will install to.
280 storagePath, err := getStorage(4096 + 128 + 128 + 1)
281 defer os.Remove(storagePath)
282 if err != nil {
283 t.Errorf(err.Error())
284 }
285
286 // Run QEMU. Expect the installer to succeed.
287 expectedOutput := "Installation completed"
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100288 result, err := runQemuWithInstaller(qemuDriveParam(storagePath), expectedOutput)
Mateusz Zalega43e21072021-10-08 18:05:29 +0200289 if err != nil {
290 t.Error(err.Error())
291 }
292 if result != true {
293 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
294 }
295
296 // Verify the resulting node image. Check whether the node GPT was created.
297 storage, err := diskfs.OpenWithMode(storagePath, diskfs.ReadOnly)
298 if err != nil {
299 t.Errorf("Couldn't open the resulting node image at %q: %s", storagePath, err.Error())
300 }
301 // Verify that GPT exists.
302 ti, err := storage.GetPartitionTable()
303 if ti.Type() != "gpt" {
304 t.Error("Couldn't verify that the resulting node image contains a GPT.")
305 }
306 // Check that the first partition is likely to be a valid ESP.
307 pi := ti.GetPartitions()
308 esp := (pi[0]).(*gpt.Partition)
309 if esp.Name != osimage.ESPVolumeLabel || esp.Start == 0 || esp.End == 0 {
310 t.Error("The node's ESP GPT entry looks off.")
311 }
312 // Verify the system partition's GPT entry.
313 system := (pi[1]).(*gpt.Partition)
314 if system.Name != osimage.SystemVolumeLabel || system.Start == 0 || system.End == 0 {
315 t.Error("The node's system partition GPT entry looks off.")
316 }
317 // Verify the data partition's GPT entry.
318 data := (pi[2]).(*gpt.Partition)
319 if data.Name != osimage.DataVolumeLabel || data.Start == 0 || data.End == 0 {
320 t.Errorf("The node's data partition GPT entry looks off.")
321 }
322 // Verify that there are no more partitions.
323 fourth := (pi[3]).(*gpt.Partition)
324 if fourth.Name != "" || fourth.Start != 0 || fourth.End != 0 {
325 t.Error("The resulting node image contains more partitions than expected.")
326 }
327 // Verify the ESP contents.
328 if err := checkEspContents(storage); err != nil {
329 t.Error(err.Error())
330 }
Lorenz Brun0b93c8d2021-11-09 03:58:40 +0100331 // Run QEMU again. Expect TestOS to launch successfully.
332 expectedOutput = "_TESTOS_LAUNCH_SUCCESS_"
333 result, err = runQemu(qemuDriveParam(storagePath), expectedOutput)
334 if err != nil {
335 t.Error(err.Error())
336 }
337 if result != true {
338 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
339 }
Mateusz Zalega43e21072021-10-08 18:05:29 +0200340}