metropolis: add Reboot RPC

This adds a new Reboot RPC to reboot a running node. It also supports
rebooting into the passive slot and powering off the node.

Change-Id: I329b22ea879adeb65a3e31103d39ad89813d61e8
Reviewed-on: https://review.monogon.dev/c/monogon/+/3354
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/metropolis/node/core/mgmt/BUILD.bazel b/metropolis/node/core/mgmt/BUILD.bazel
index a75dff9..b95565b 100644
--- a/metropolis/node/core/mgmt/BUILD.bazel
+++ b/metropolis/node/core/mgmt/BUILD.bazel
@@ -4,6 +4,7 @@
     name = "mgmt",
     srcs = [
         "mgmt.go",
+        "power.go",
         "svc_logs.go",
         "update.go",
     ],
@@ -16,6 +17,7 @@
         "//metropolis/node/core/update",
         "//metropolis/proto/api",
         "//metropolis/proto/common",
+        "//osbase/efivarfs",
         "//osbase/logtree",
         "//osbase/logtree/proto",
         "//osbase/supervisor",
diff --git a/metropolis/node/core/mgmt/power.go b/metropolis/node/core/mgmt/power.go
new file mode 100644
index 0000000..77601d7
--- /dev/null
+++ b/metropolis/node/core/mgmt/power.go
@@ -0,0 +1,68 @@
+package mgmt
+
+import (
+	"context"
+	"os"
+	"time"
+
+	"golang.org/x/sys/unix"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	apb "source.monogon.dev/metropolis/proto/api"
+	"source.monogon.dev/osbase/efivarfs"
+)
+
+func (s *Service) Reboot(ctx context.Context, req *apb.RebootRequest) (*apb.RebootResponse, error) {
+	var method int
+	// Do not yet perform any system-wide actions here as the request might
+	// still get rejected. There is another switch statement for that below.
+	switch req.Type {
+	case apb.RebootRequest_KEXEC:
+		method = unix.LINUX_REBOOT_CMD_KEXEC
+	case apb.RebootRequest_FIRMWARE:
+		method = unix.LINUX_REBOOT_CMD_RESTART
+	case apb.RebootRequest_POWER_OFF:
+		method = unix.LINUX_REBOOT_CMD_POWER_OFF
+	default:
+		return nil, status.Error(codes.Unimplemented, "unimplemented type value")
+	}
+	switch req.NextBoot {
+	case apb.RebootRequest_START_NORMAL:
+	case apb.RebootRequest_START_ROLLBACK:
+		if err := s.UpdateService.Rollback(); err != nil {
+			return nil, status.Errorf(codes.Unavailable, "performing rollback failed: %v", err)
+		}
+	case apb.RebootRequest_START_FIRMWARE_UI:
+		if req.Type == apb.RebootRequest_KEXEC {
+			return nil, status.Error(codes.InvalidArgument, "START_FIRMWARE_UI cannot be used with KEXEC type")
+		}
+		supp, err := efivarfs.OSIndicationsSupported()
+		if err != nil || supp&efivarfs.BootToFirmwareUI == 0 {
+			return nil, status.Error(codes.Unimplemented, "Unable to boot into firmware UI on this platform")
+		}
+		if err := efivarfs.SetOSIndications(efivarfs.BootToFirmwareUI); err != nil {
+			return nil, status.Errorf(codes.Unavailable, "Unable to set UEFI boot to UI indication: %v", err)
+		}
+	default:
+		return nil, status.Error(codes.Unimplemented, "unimplemented next_boot value")
+	}
+
+	switch req.Type {
+	case apb.RebootRequest_KEXEC:
+		if err := s.UpdateService.KexecLoadNext(); err != nil {
+			return nil, status.Errorf(codes.Unavailable, "failed to stage kexec kernel: %v", err)
+		}
+	case apb.RebootRequest_FIRMWARE:
+		// Best-effort, if it fails this will still be a firmware reboot.
+		os.WriteFile("/sys/kernel/reboot/mode", []byte("cold"), 0644)
+	}
+	s.LogTree.MustLeveledFor("root.mgmt").Warning("Reboot requested, rebooting in 2s")
+	go func() {
+		time.Sleep(2 * time.Second)
+		unix.Unmount(s.UpdateService.ESPPath, 0)
+		unix.Sync()
+		unix.Reboot(method)
+	}()
+	return &apb.RebootResponse{}, nil
+}