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/BUILD.bazel b/osbase/bringup/BUILD.bazel
new file mode 100644
index 0000000..89b40ff
--- /dev/null
+++ b/osbase/bringup/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("//osbase/build:def.bzl", "node_initramfs")
+
+go_library(
+ name = "bringup",
+ srcs = ["bringup.go"],
+ importpath = "source.monogon.dev/osbase/bringup",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//osbase/bootparam",
+ "//osbase/efivarfs",
+ "//osbase/logtree",
+ "//osbase/supervisor",
+ "@org_golang_x_sys//unix",
+ "@org_uber_go_multierr//:multierr",
+ ],
+)
diff --git a/osbase/bringup/bringup.go b/osbase/bringup/bringup.go
new file mode 100644
index 0000000..54bbad6
--- /dev/null
+++ b/osbase/bringup/bringup.go
@@ -0,0 +1,132 @@
+// Package bringup implements a simple wrapper which configures all default
+// mounts, logging and the corresponding forwarders to tty0 and ttyS0. It
+// then configures a new logtree and starts a supervisor to run the provided
+// supervisor.Runnable. Said Runnable is expected to never return. If it does,
+// the supervisor will exit, an error will be printed and the system will
+// reboot after five seconds.
+package bringup
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ "go.uber.org/multierr"
+ "golang.org/x/sys/unix"
+
+ "source.monogon.dev/osbase/bootparam"
+ "source.monogon.dev/osbase/efivarfs"
+ "source.monogon.dev/osbase/logtree"
+ "source.monogon.dev/osbase/supervisor"
+)
+
+type Runnable supervisor.Runnable
+
+func (r Runnable) Run() {
+ // Pause execution on panic to require manual intervention.
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Printf("Fatal error: %v\n", r)
+ fmt.Printf("This node could not be started. Rebooting...\n")
+
+ time.Sleep(5 * time.Second)
+ unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
+ }
+ }()
+
+ if err := setupMounts(); err != nil {
+ // We cannot do anything if we fail to mount.
+ panic(err)
+ }
+
+ // Set up logger. Parse consoles from the kernel command line
+ // as well as adding the two standard tty0/ttyS0 consoles.
+ consoles := make(map[string]bool)
+ cmdline, err := os.ReadFile("/proc/cmdline")
+ if err == nil {
+ params, _, err := bootparam.Unmarshal(string(cmdline))
+ if err == nil {
+ consoles = params.Consoles()
+ }
+ }
+ consoles["tty0"] = true
+ consoles["ttyS0"] = true
+
+ lt := logtree.New()
+ for consolePath := range consoles {
+ f, err := os.OpenFile("/dev/"+consolePath, os.O_WRONLY, 0)
+ if err != nil {
+ continue
+ }
+ reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
+ if err != nil {
+ panic(fmt.Errorf("could not set up root log reader: %v", err))
+ }
+ go func() {
+ for {
+ p := <-reader.Stream
+ fmt.Fprintf(f, "%s\n", p.String())
+ }
+ }()
+ }
+
+ sCtx, cancel := context.WithCancelCause(context.Background())
+
+ // Don't reschedule the root runnable...
+ supervisor.New(sCtx, func(ctx context.Context) (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("root runnable paniced: %v", r)
+ cancel(err)
+ }
+ }()
+
+ err = r(ctx)
+ if err == nil {
+ err = fmt.Errorf("root runnable exited without any error")
+ }
+
+ cancel(err)
+ return nil
+ }, supervisor.WithExistingLogtree(lt))
+
+ <-sCtx.Done()
+ panic(context.Cause(sCtx))
+}
+
+func mkdirAndMount(dir, fs string, flags uintptr) error {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return fmt.Errorf("could not make %s: %w", dir, err)
+ }
+ if err := unix.Mount(fs, dir, fs, flags, ""); err != nil {
+ return fmt.Errorf("could not mount %s on %s: %w", fs, dir, err)
+ }
+ return nil
+}
+
+// setupMounts sets up basic mounts like sysfs, procfs, devtmpfs and cgroups.
+// This should be called early during init as a lot of processes depend on this
+// being available.
+func setupMounts() (err error) {
+ // Set up target filesystems.
+ for _, el := range []struct {
+ dir string
+ fs string
+ flags uintptr
+ }{
+ {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
+ {"/sys/kernel/tracing", "tracefs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
+ {"/sys/fs/pstore", "pstore", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
+ {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
+ {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
+ {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
+ } {
+ err = multierr.Append(err, mkdirAndMount(el.dir, el.fs, el.flags))
+ }
+
+ // We try to mount efivarfs but ignore any error,
+ // as we don't want to crash on non-EFI systems.
+ _ = mkdirAndMount(efivarfs.Path, "efivarfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV)
+ return
+}
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)
+ }
+}