m/n/c/devmgr: init

Add a minimal device manager based on kobject/uevents. Currently this
only loads kernel modules. Further functionality will be added in
subsequent CLs.

Change-Id: I444ecdaff3f8ddb9ec169b094ba03e169dd70c4e
Reviewed-on: https://review.monogon.dev/c/monogon/+/1790
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/core/devmgr/BUILD.bazel b/metropolis/node/core/devmgr/BUILD.bazel
new file mode 100644
index 0000000..15b8065
--- /dev/null
+++ b/metropolis/node/core/devmgr/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "devmgr",
+    srcs = ["devmgr.go"],
+    importpath = "source.monogon.dev/metropolis/node/core/devmgr",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//metropolis/pkg/kmod",
+        "//metropolis/pkg/supervisor",
+        "@com_github_mdlayher_kobject//:kobject",
+    ],
+)
diff --git a/metropolis/node/core/devmgr/devmgr.go b/metropolis/node/core/devmgr/devmgr.go
new file mode 100644
index 0000000..fc626b0
--- /dev/null
+++ b/metropolis/node/core/devmgr/devmgr.go
@@ -0,0 +1,78 @@
+// Package devmgr is the userspace pendant to the kernel device management
+// system. It talks to the kernel and the performs any further userspace actions
+// related to device events. It corresponds very roughly to systemd-udevd on
+// more conventional Linux distros. It currently only handles dynamic module
+// loading, but will be extended to provide better device handling in other
+// parts of the system.
+package devmgr
+
+import (
+	"context"
+	"fmt"
+	"io/fs"
+	"os"
+	"path/filepath"
+
+	"github.com/mdlayher/kobject"
+
+	"source.monogon.dev/metropolis/pkg/kmod"
+	"source.monogon.dev/metropolis/pkg/supervisor"
+)
+
+type Service struct{}
+
+func New() *Service {
+	return &Service{}
+}
+
+func (s *Service) Run(ctx context.Context) error {
+	c, err := kobject.New()
+	if err != nil {
+		return fmt.Errorf("unable to create kobject uevent socket: %w", err)
+	}
+	defer c.Close()
+
+	l := supervisor.Logger(ctx)
+
+	modMgr, err := kmod.NewManagerFromPath("/lib/modules")
+	if err != nil {
+		return fmt.Errorf("error creating module manager: %w", err)
+	}
+
+	// Start goroutine which instructs the kernel to generate "synthetic"
+	// uevents for all preexisting devices. This allows the kobject netlink
+	// listener below to "catch up" on devices added before it was created.
+	// This functionality is minimally documented in the Linux kernel, the
+	// closest we have is
+	// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-uevent which
+	// contains documentation on how to trigger synthetic events.
+	go func() {
+		err = filepath.WalkDir("/sys/devices", func(path string, d fs.DirEntry, err error) error {
+			if !d.IsDir() && d.Name() == "uevent" {
+				if err := os.WriteFile(path, []byte("add"), 0); err != nil {
+					return err
+				}
+			}
+			return nil
+		})
+		if err != nil {
+			l.Errorf("failed to load initial device database: %v", err)
+		} else {
+			l.Info("Initial device loading done")
+		}
+	}()
+
+	for {
+		e, err := c.Receive()
+		if err != nil {
+			return fmt.Errorf("error receiving kobject uevent: %w", err)
+		}
+		if e.Action == kobject.Add {
+			if e.Values["MODALIAS"] != "" {
+				if err := modMgr.LoadModulesForDevice(e.Values["MODALIAS"]); err != nil {
+					l.Errorf("Error loading kernel modules: %w", err)
+				}
+			}
+		}
+	}
+}