m/{t,n}/installer: use bundles and test install

This makes the installer use actual bundles and uses TestOS bundles
to test the installation process end-to-end.

Change-Id: I64fa412032796d7d7633e9944dbae727d90a863e
Reviewed-on: https://review.monogon.dev/c/monogon/+/433
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/node/installer/main.go b/metropolis/node/installer/main.go
index 356247a..6eb6706 100644
--- a/metropolis/node/installer/main.go
+++ b/metropolis/node/installer/main.go
@@ -20,6 +20,7 @@
 package main
 
 import (
+	"archive/zip"
 	"fmt"
 	"io"
 	"log"
@@ -196,6 +197,28 @@
 		log.Fatalf("while mounting the installer ESP: %s", err.Error())
 	}
 
+	nodeParameters, err := os.Open("/installer/EFI/metropolis-installer/nodeparams.pb")
+	if err != nil {
+		log.Fatalf("failed to open node parameters from ESP: %v", err)
+	}
+
+	// TODO(lorenz): Replace with proper bundles
+	bundle, err := zip.OpenReader("/installer/EFI/metropolis-installer/bundle.bin")
+	if err != nil {
+		log.Fatalf("failed to open node bundle from ESP: %v", err)
+	}
+	defer bundle.Close()
+	efiPayload, err := bundle.Open("kernel_efi.efi")
+	if err != nil {
+		log.Fatalf("Cannot open EFI payload in bundle: %v", err)
+	}
+	defer efiPayload.Close()
+	systemImage, err := bundle.Open("rootfs.img")
+	if err != nil {
+		log.Fatalf("Cannot open system image in bundle: %v", err)
+	}
+	defer systemImage.Close()
+
 	// Build the osimage parameters.
 	installParams := osimage.Params{
 		PartitionSize: osimage.PartitionSizeInfo{
@@ -215,9 +238,9 @@
 		// TODO(mateusz@monogon.tech): Address that bug either by patching go-diskfs
 		// or rewriting osimage.
 		SystemImage: nil,
-		// TODO(mateusz@monogon.tech): Plug in.
-		EFIPayload:     strings.NewReader("TODO"),
-		NodeParameters: strings.NewReader("TODO"),
+
+		EFIPayload:     efiPayload,
+		NodeParameters: nodeParameters,
 	}
 	// Calculate the minimum target size based on the installation parameters.
 	minSize := uint64((installParams.PartitionSize.ESP +
@@ -259,8 +282,7 @@
 	}
 	sysBlkdevPath := filepath.Join("/dev", sysBlkdevName)
 	// Copy the system partition contents.
-	contents := strings.NewReader("TODO") // TODO(mz): plug in
-	if err := initializeSystemPartition(contents, sysBlkdevPath); err != nil {
+	if err := initializeSystemPartition(systemImage, sysBlkdevPath); err != nil {
 		log.Fatalf("while initializing the system partition at %q: %s", sysBlkdevPath, err.Error())
 	}
 
diff --git a/metropolis/test/installer/BUILD.bazel b/metropolis/test/installer/BUILD.bazel
index 28ede12..c6bc09e 100644
--- a/metropolis/test/installer/BUILD.bazel
+++ b/metropolis/test/installer/BUILD.bazel
@@ -5,6 +5,7 @@
     size = "small",
     data = [
         "//metropolis/node/installer:kernel",
+        "//metropolis/test/installer/testos:testos_bundle",
         "//third_party/edk2:firmware",
         "@qemu//:qemu-x86_64-softmmu",
     ],
@@ -20,6 +21,7 @@
     deps = [
         "//metropolis/cli/metroctl/core:go_default_library",
         "//metropolis/node/build/mkimage/osimage:go_default_library",
+        "//metropolis/proto/api:go_default_library",
         "@com_github_diskfs_go_diskfs//:go_default_library",
         "@com_github_diskfs_go_diskfs//disk:go_default_library",
         "@com_github_diskfs_go_diskfs//partition/gpt:go_default_library",
diff --git a/metropolis/test/installer/main.go b/metropolis/test/installer/main.go
index f31290d..ea2c20a 100644
--- a/metropolis/test/installer/main.go
+++ b/metropolis/test/installer/main.go
@@ -33,12 +33,15 @@
 	diskfs "github.com/diskfs/go-diskfs"
 	"github.com/diskfs/go-diskfs/disk"
 	"github.com/diskfs/go-diskfs/partition/gpt"
+	"source.monogon.dev/metropolis/proto/api"
+
 	mctl "source.monogon.dev/metropolis/cli/metroctl/core"
 	osimage "source.monogon.dev/metropolis/node/build/mkimage/osimage"
 )
 
 const (
 	InstallerEFIPayload = "metropolis/node/installer/kernel.efi"
+	TestOSBundle        = "metropolis/test/installer/testos/testos_bundle.zip"
 	InstallerImage      = "metropolis/test/installer/installer.img"
 	NodeStorage         = "metropolis/test/installer/stor.img"
 )
@@ -55,7 +58,6 @@
 		"-cpu", "host",
 		"-drive", "if=pflash,format=raw,readonly,file=external/edk2/OVMF_CODE.fd",
 		"-drive", "if=pflash,format=raw,snapshot=on,file=external/edk2/OVMF_VARS.fd",
-		"-drive", "if=virtio,format=raw,snapshot=on,cache=unsafe,file=" + InstallerImage,
 		"-serial", "stdio",
 		"-no-reboot",
 	}
@@ -83,6 +85,14 @@
 	return result, nil
 }
 
+// runQemuWithInstaller starts a QEMU process and waits for it to finish. args is
+// concatenated to the list of predefined default arguments. It returns true if
+// expectedOutput is found in the serial port output. It may return an error.
+func runQemuWithInstaller(args []string, expectedOutput string) (bool, error) {
+	args = append(args, "-drive", "if=virtio,format=raw,snapshot=on,cache=unsafe,file="+InstallerImage)
+	return runQemu(args, expectedOutput)
+}
+
 // getStorage creates a sparse file, given a size expressed in mebibytes, and
 // returns a path to that file. It may return an error.
 func getStorage(size int64) (string, error) {
@@ -132,10 +142,21 @@
 	if err != nil {
 		log.Fatalf("Couldn't stat the installer EFI executable: %s", err.Error())
 	}
+	bundle, err := os.Open(TestOSBundle)
+	if err != nil {
+		log.Fatalf("failed to open TestOS bundle: %v", err)
+	}
+	bundleStat, err := bundle.Stat()
+	if err != nil {
+		log.Fatalf("failed to stat() TestOS bundle: %v", err)
+	}
 	iargs := mctl.MakeInstallerImageArgs{
 		Installer:     installer,
 		InstallerSize: uint64(info.Size()),
 		TargetPath:    InstallerImage,
+		NodeParams:    &api.NodeParameters{},
+		Bundle:        bundle,
+		BundleSize:    uint64(bundleStat.Size()),
 	}
 	if err := mctl.MakeInstallerImage(iargs); err != nil {
 		log.Fatalf("Couldn't create the installer image at %q: %s", InstallerImage, err.Error())
@@ -181,7 +202,7 @@
 	// the installer to fail at the device probe stage rather than attempting to
 	// use the medium as the target device.
 	expectedOutput := "couldn't find a suitable block device"
-	result, err := runQemu(nil, expectedOutput)
+	result, err := runQemuWithInstaller(nil, expectedOutput)
 	if err != nil {
 		t.Error(err.Error())
 	}
@@ -201,7 +222,7 @@
 
 	// Run QEMU. Expect the installer to fail with a predefined error string.
 	expectedOutput := "couldn't find a suitable block device"
-	result, err := runQemu(qemuDriveParam(imagePath), expectedOutput)
+	result, err := runQemuWithInstaller(qemuDriveParam(imagePath), expectedOutput)
 	if err != nil {
 		t.Error(err.Error())
 	}
@@ -220,7 +241,7 @@
 
 	// Run QEMU. Expect the installer to succeed.
 	expectedOutput := "Installation completed"
-	result, err := runQemu(qemuDriveParam(storagePath), expectedOutput)
+	result, err := runQemuWithInstaller(qemuDriveParam(storagePath), expectedOutput)
 	if err != nil {
 		t.Error(err.Error())
 	}
@@ -263,4 +284,13 @@
 	if err := checkEspContents(storage); err != nil {
 		t.Error(err.Error())
 	}
+	// Run QEMU again. Expect TestOS to launch successfully.
+	expectedOutput = "_TESTOS_LAUNCH_SUCCESS_"
+	result, err = runQemu(qemuDriveParam(storagePath), expectedOutput)
+	if err != nil {
+		t.Error(err.Error())
+	}
+	if result != true {
+		t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
+	}
 }