blob: f31290d237a9bf3a987b956a0f8f14ebbc6873c3 [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"
30 "syscall"
31 "testing"
32
33 diskfs "github.com/diskfs/go-diskfs"
34 "github.com/diskfs/go-diskfs/disk"
35 "github.com/diskfs/go-diskfs/partition/gpt"
36 mctl "source.monogon.dev/metropolis/cli/metroctl/core"
37 osimage "source.monogon.dev/metropolis/node/build/mkimage/osimage"
38)
39
40const (
41 InstallerEFIPayload = "metropolis/node/installer/kernel.efi"
42 InstallerImage = "metropolis/test/installer/installer.img"
43 NodeStorage = "metropolis/test/installer/stor.img"
44)
45
46// runQemu starts a QEMU process and waits for it to finish. args is
47// concatenated to the list of predefined default arguments. It returns true if
48// expectedOutput is found in the serial port output. It may return an error.
49func runQemu(args []string, expectedOutput string) (bool, error) {
50 // Prepare the default parameter list.
51 defaultArgs := []string{
52 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults",
53 "-m", "512",
54 "-smp", "2",
55 "-cpu", "host",
56 "-drive", "if=pflash,format=raw,readonly,file=external/edk2/OVMF_CODE.fd",
57 "-drive", "if=pflash,format=raw,snapshot=on,file=external/edk2/OVMF_VARS.fd",
58 "-drive", "if=virtio,format=raw,snapshot=on,cache=unsafe,file=" + InstallerImage,
59 "-serial", "stdio",
60 "-no-reboot",
61 }
62 // Join the parameter lists and prepare the Qemu command, but don't run it
63 // just yet.
64 qemuArgs := append(defaultArgs, args...)
65 qemuCmd := exec.Command("external/qemu/qemu-x86_64-softmmu", qemuArgs...)
66
67 // Copy the stdout and stderr output so that it could be matched against
68 // expectedOutput later.
69 var outBuf, errBuf bytes.Buffer
70 outWriter := bufio.NewWriter(&outBuf)
71 errWriter := bufio.NewWriter(&errBuf)
72 qemuCmd.Stdout = io.MultiWriter(os.Stdout, outWriter)
73 qemuCmd.Stderr = io.MultiWriter(os.Stderr, errWriter)
74 if err := qemuCmd.Run(); err != nil {
75 return false, fmt.Errorf("couldn't start QEMU: %w", err)
76 }
77 outWriter.Flush()
78 errWriter.Flush()
79
80 // Try matching against expectedOutput and return the result.
81 result := bytes.Contains(outBuf.Bytes(), []byte(expectedOutput)) ||
82 bytes.Contains(errBuf.Bytes(), []byte(expectedOutput))
83 return result, nil
84}
85
86// getStorage creates a sparse file, given a size expressed in mebibytes, and
87// returns a path to that file. It may return an error.
88func getStorage(size int64) (string, error) {
89 image, err := os.Create(NodeStorage)
90 if err != nil {
91 return "", fmt.Errorf("couldn't create the block device image at %q: %w", NodeStorage, err)
92 }
93 if err := syscall.Ftruncate(int(image.Fd()), size*1024*1024); err != nil {
94 return "", fmt.Errorf("couldn't resize the block device image at %q: %w", NodeStorage, err)
95 }
96 image.Close()
97 return NodeStorage, nil
98}
99
100// qemuDriveParam returns QEMU parameters required to run it with a
101// raw-format image at path.
102func qemuDriveParam(path string) []string {
103 return []string{"-drive", "if=virtio,format=raw,snapshot=off,cache=unsafe,file=" + path}
104}
105
106// checkEspContents verifies the presence of the EFI payload inside of image's
107// first partition. It returns nil on success.
108func checkEspContents(image *disk.Disk) error {
109 // Get the ESP.
110 fs, err := image.GetFilesystem(1)
111 if err != nil {
112 return fmt.Errorf("couldn't read the installer ESP: %w", err)
113 }
114 // Make sure the EFI payload exists by attempting to open it.
115 efiPayload, err := fs.OpenFile(osimage.EFIPayloadPath, os.O_RDONLY)
116 if err != nil {
117 return fmt.Errorf("couldn't open the installer's EFI Payload at %q: %w", osimage.EFIPayloadPath, err)
118 }
119 efiPayload.Close()
120 return nil
121}
122
123func TestMain(m *testing.M) {
124 // Build the installer image with metroctl, given the EFI executable
125 // generated by Metropolis buildsystem. This mimics standard usage of
126 // metroctl CLI.
127 installer, err := os.Open(InstallerEFIPayload)
128 if err != nil {
129 log.Fatalf("Couldn't open the installer EFI executable at %q: %s", InstallerEFIPayload, err.Error())
130 }
131 info, err := installer.Stat()
132 if err != nil {
133 log.Fatalf("Couldn't stat the installer EFI executable: %s", err.Error())
134 }
135 iargs := mctl.MakeInstallerImageArgs{
136 Installer: installer,
137 InstallerSize: uint64(info.Size()),
138 TargetPath: InstallerImage,
139 }
140 if err := mctl.MakeInstallerImage(iargs); err != nil {
141 log.Fatalf("Couldn't create the installer image at %q: %s", InstallerImage, err.Error())
142 }
143 // With common dependencies set up, run the tests.
144 code := m.Run()
145 // Clean up.
146 os.Remove(InstallerImage)
147 os.Exit(code)
148}
149
150func TestInstallerImage(t *testing.T) {
151 // This test examines the installer image, making sure that the GPT and the
152 // ESP contents are in order.
153 image, err := diskfs.OpenWithMode(InstallerImage, diskfs.ReadOnly)
154 if err != nil {
155 t.Errorf("Couldn't open the installer image at %q: %s", InstallerImage, err.Error())
156 }
157 // Verify that GPT exists.
158 ti, err := image.GetPartitionTable()
159 if ti.Type() != "gpt" {
160 t.Error("Couldn't verify that the installer image contains a GPT.")
161 }
162 // Check that the first partition is likely to be a valid ESP.
163 pi := ti.GetPartitions()
164 esp := (pi[0]).(*gpt.Partition)
165 if esp.Start == 0 || esp.End == 0 {
166 t.Error("The installer's ESP GPT entry looks off.")
167 }
168 // Verify that the image contains only one partition.
169 second := (pi[1]).(*gpt.Partition)
170 if second.Name != "" || second.Start != 0 || second.End != 0 {
171 t.Error("It appears the installer image contains more than one partition.")
172 }
173 // Verify the ESP contents.
174 if err := checkEspContents(image); err != nil {
175 t.Error(err.Error())
176 }
177}
178
179func TestNoBlockDevices(t *testing.T) {
180 // No block devices are passed to QEMU aside from the install medium. Expect
181 // the installer to fail at the device probe stage rather than attempting to
182 // use the medium as the target device.
183 expectedOutput := "couldn't find a suitable block device"
184 result, err := runQemu(nil, expectedOutput)
185 if err != nil {
186 t.Error(err.Error())
187 }
188 if result != true {
189 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
190 }
191}
192
193func TestBlockDeviceTooSmall(t *testing.T) {
194 // Prepare the block device the installer will install to. This time the
195 // target device is too small to host a Metropolis installation.
196 imagePath, err := getStorage(64)
197 defer os.Remove(imagePath)
198 if err != nil {
199 t.Errorf(err.Error())
200 }
201
202 // Run QEMU. Expect the installer to fail with a predefined error string.
203 expectedOutput := "couldn't find a suitable block device"
204 result, err := runQemu(qemuDriveParam(imagePath), expectedOutput)
205 if err != nil {
206 t.Error(err.Error())
207 }
208 if result != true {
209 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
210 }
211}
212
213func TestInstall(t *testing.T) {
214 // Prepare the block device image the installer will install to.
215 storagePath, err := getStorage(4096 + 128 + 128 + 1)
216 defer os.Remove(storagePath)
217 if err != nil {
218 t.Errorf(err.Error())
219 }
220
221 // Run QEMU. Expect the installer to succeed.
222 expectedOutput := "Installation completed"
223 result, err := runQemu(qemuDriveParam(storagePath), expectedOutput)
224 if err != nil {
225 t.Error(err.Error())
226 }
227 if result != true {
228 t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
229 }
230
231 // Verify the resulting node image. Check whether the node GPT was created.
232 storage, err := diskfs.OpenWithMode(storagePath, diskfs.ReadOnly)
233 if err != nil {
234 t.Errorf("Couldn't open the resulting node image at %q: %s", storagePath, err.Error())
235 }
236 // Verify that GPT exists.
237 ti, err := storage.GetPartitionTable()
238 if ti.Type() != "gpt" {
239 t.Error("Couldn't verify that the resulting node image contains a GPT.")
240 }
241 // Check that the first partition is likely to be a valid ESP.
242 pi := ti.GetPartitions()
243 esp := (pi[0]).(*gpt.Partition)
244 if esp.Name != osimage.ESPVolumeLabel || esp.Start == 0 || esp.End == 0 {
245 t.Error("The node's ESP GPT entry looks off.")
246 }
247 // Verify the system partition's GPT entry.
248 system := (pi[1]).(*gpt.Partition)
249 if system.Name != osimage.SystemVolumeLabel || system.Start == 0 || system.End == 0 {
250 t.Error("The node's system partition GPT entry looks off.")
251 }
252 // Verify the data partition's GPT entry.
253 data := (pi[2]).(*gpt.Partition)
254 if data.Name != osimage.DataVolumeLabel || data.Start == 0 || data.End == 0 {
255 t.Errorf("The node's data partition GPT entry looks off.")
256 }
257 // Verify that there are no more partitions.
258 fourth := (pi[3]).(*gpt.Partition)
259 if fourth.Name != "" || fourth.Start != 0 || fourth.End != 0 {
260 t.Error("The resulting node image contains more partitions than expected.")
261 }
262 // Verify the ESP contents.
263 if err := checkEspContents(storage); err != nil {
264 t.Error(err.Error())
265 }
266}