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/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
+}