osbase/bringup: add bringup

Introduce a library which handles the bringup of a running environment
for supervisor runnables.

Change-Id: I03c049d1bac7afdc71dfa24247923070982f07cd
Reviewed-on: https://review.monogon.dev/c/monogon/+/2930
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/bringup/test/BUILD.bazel b/osbase/bringup/test/BUILD.bazel
new file mode 100644
index 0000000..7e010e0
--- /dev/null
+++ b/osbase/bringup/test/BUILD.bazel
@@ -0,0 +1,111 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+load("//osbase/build:def.bzl", "node_initramfs")
+load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
+
+go_test(
+    name = "test_test",
+    size = "medium",
+    srcs = ["run_test.go"],
+    data = [
+        ":kernel_failed",
+        ":kernel_succeeded",
+        "//third_party/edk2:OVMF_CODE.fd",
+        "//third_party/edk2:OVMF_VARS.fd",
+        "@qemu//:qemu-x86_64-softmmu",
+    ],
+    importpath = "source.monogon.dev/metropolis/installer/test",
+    visibility = ["//visibility:private"],
+    x_defs = {
+        "xOvmfVarsPath": "$(rlocationpath //third_party/edk2:OVMF_VARS.fd )",
+        "xOvmfCodePath": "$(rlocationpath //third_party/edk2:OVMF_CODE.fd )",
+        "xQemuPath": "$(rlocationpath @qemu//:qemu-x86_64-softmmu )",
+        "xSucceedKernelPath": "$(rlocationpath :kernel_succeeded )",
+        "xFailedKernelPath": "$(rlocationpath :kernel_failed )",
+    },
+    deps = [
+        "//osbase/cmd",
+        "@io_bazel_rules_go//go/runfiles:go_default_library",
+    ],
+)
+
+go_library(
+    name = "succeeded_lib",
+    srcs = ["main_succeeded.go"],
+    importpath = "source.monogon.dev/osbase/bringup/test",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//osbase/bootparam",
+        "//osbase/bringup",
+        "//osbase/efivarfs",
+        "//osbase/logtree",
+        "//osbase/supervisor",
+        "@org_golang_x_sys//unix",
+        "@org_uber_go_multierr//:multierr",
+    ],
+)
+
+go_binary(
+    name = "succeeded",
+    embed = [":succeeded_lib"],
+    visibility = ["//visibility:private"],
+)
+
+node_initramfs(
+    name = "initramfs_succeeded",
+    files = {
+        ":succeeded": "/init",
+    },
+    fsspecs = [
+        "//osbase/build:earlydev.fsspec",
+    ],
+    visibility = ["//visibility:private"],
+)
+
+efi_unified_kernel_image(
+    name = "kernel_succeeded",
+    cmdline = "quiet console=ttyS0 init=/init",
+    initrd = [":initramfs_succeeded"],
+    kernel = "//third_party/linux",
+    visibility = ["//visibility:private"],
+)
+
+go_library(
+    name = "failed_lib",
+    srcs = ["main_failed.go"],
+    importpath = "source.monogon.dev/osbase/bringup/test",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//osbase/bootparam",
+        "//osbase/bringup",
+        "//osbase/efivarfs",
+        "//osbase/logtree",
+        "//osbase/supervisor",
+        "@org_golang_x_sys//unix",
+        "@org_uber_go_multierr//:multierr",
+    ],
+)
+
+go_binary(
+    name = "failed",
+    embed = [":failed_lib"],
+    visibility = ["//visibility:private"],
+)
+
+node_initramfs(
+    name = "initramfs_failed",
+    files = {
+        ":failed": "/init",
+    },
+    fsspecs = [
+        "//osbase/build:earlydev.fsspec",
+    ],
+    visibility = ["//visibility:private"],
+)
+
+efi_unified_kernel_image(
+    name = "kernel_failed",
+    cmdline = "quiet console=ttyS0 init=/init",
+    initrd = [":initramfs_failed"],
+    kernel = "//third_party/linux",
+    visibility = ["//visibility:private"],
+)
diff --git a/osbase/bringup/test/main_failed.go b/osbase/bringup/test/main_failed.go
new file mode 100644
index 0000000..4ed8a17
--- /dev/null
+++ b/osbase/bringup/test/main_failed.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+	"source.monogon.dev/osbase/bringup"
+)
+
+func main() {
+	bringup.Runnable(nil).Run()
+}
diff --git a/osbase/bringup/test/main_succeeded.go b/osbase/bringup/test/main_succeeded.go
new file mode 100644
index 0000000..930455c
--- /dev/null
+++ b/osbase/bringup/test/main_succeeded.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"golang.org/x/sys/unix"
+
+	"source.monogon.dev/osbase/bringup"
+)
+
+func main() {
+	bringup.Runnable(func(ctx context.Context) error {
+		fmt.Println("_BRINGUP_LAUNCH_SUCCESS_")
+		unix.Reboot(unix.LINUX_REBOOT_CMD_POWER_OFF)
+		return nil
+	}).Run()
+}
diff --git a/osbase/bringup/test/run_test.go b/osbase/bringup/test/run_test.go
new file mode 100644
index 0000000..3252c54
--- /dev/null
+++ b/osbase/bringup/test/run_test.go
@@ -0,0 +1,90 @@
+package test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/bazelbuild/rules_go/go/runfiles"
+
+	"source.monogon.dev/osbase/cmd"
+)
+
+var (
+	// These are filled by bazel at linking time with the canonical path of
+	// their corresponding file. Inside the init function we resolve it
+	// with the rules_go runfiles package to the real path.
+	xOvmfCodePath      string
+	xOvmfVarsPath      string
+	xQemuPath          string
+	xSucceedKernelPath string
+	xFailedKernelPath  string
+)
+
+func init() {
+	var err error
+	for _, path := range []*string{
+		&xOvmfCodePath, &xOvmfVarsPath, &xQemuPath,
+		&xSucceedKernelPath, &xFailedKernelPath,
+	} {
+		*path, err = runfiles.Rlocation(*path)
+		if err != nil {
+			panic(err)
+		}
+	}
+}
+
+// runQemu starts a new QEMU process, expecting the given output to appear
+// in any line printed. It returns true, if the expected string was found,
+// and false otherwise.
+//
+// QEMU is killed shortly after the string is found, or when the context is
+// cancelled.
+func runQemu(ctx context.Context, args []string, expectedOutput string) (bool, error) {
+	defaultArgs := []string{
+		"-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults",
+		"-m", "512",
+		"-smp", "2",
+		"-cpu", "host",
+		"-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfCodePath,
+		"-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfVarsPath,
+		"-serial", "stdio",
+		"-no-reboot",
+	}
+	qemuArgs := append(defaultArgs, args...)
+	pf := cmd.TerminateIfFound(expectedOutput, nil)
+	return cmd.RunCommand(ctx, xQemuPath, qemuArgs, pf)
+}
+
+func TestBringupSuccess(t *testing.T) {
+	ctx, ctxC := context.WithTimeout(context.Background(), time.Second*30)
+	defer ctxC()
+
+	extraArgs := append([]string(nil), "-kernel", xSucceedKernelPath)
+
+	// Run QEMU. Expect the installer to succeed with a predefined error string.
+	expectedOutput := "_BRINGUP_LAUNCH_SUCCESS_"
+	result, err := runQemu(ctx, extraArgs, expectedOutput)
+	if err != nil {
+		t.Error(err.Error())
+	}
+	if !result {
+		t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
+	}
+}
+func TestBringupFailed(t *testing.T) {
+	ctx, ctxC := context.WithTimeout(context.Background(), time.Second*30)
+	defer ctxC()
+
+	extraArgs := append([]string(nil), "-kernel", xFailedKernelPath)
+
+	// Run QEMU. Expect the installer to fail with a predefined error string.
+	expectedOutput := "root runnable paniced"
+	result, err := runQemu(ctx, extraArgs, expectedOutput)
+	if err != nil {
+		t.Error(err.Error())
+	}
+	if !result {
+		t.Errorf("QEMU didn't produce the expected output %q", expectedOutput)
+	}
+}