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/go.mod b/go.mod
index 19e6f52..0fd7bb9 100644
--- a/go.mod
+++ b/go.mod
@@ -294,6 +294,7 @@
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
+	github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d
 	github.com/mdlayher/packet v0.0.0-20220221164757-67998ac0ff93 // indirect
 	github.com/mdlayher/socket v0.2.1 // indirect
 	github.com/miekg/dns v1.1.48 // indirect
diff --git a/go.sum b/go.sum
index 4af2081..bbec387 100644
--- a/go.sum
+++ b/go.sum
@@ -1542,6 +1542,8 @@
 github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
 github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
 github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
+github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d h1:JmrZTpS0GAyMV4ZQVVH/AS0Y6r2PbnYNSRUuRX+HOLA=
+github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc=
 github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
 github.com/mdlayher/netlink v0.0.0-20190516121005-0087c778e469/go.mod h1:gOrA34zDL0K3RsACQe54bDYLF/CeFspQ9m5DOycycQ8=
 github.com/mdlayher/netlink v0.0.0-20190828143259-340058475d09/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
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)
+				}
+			}
+		}
+	}
+}
diff --git a/third_party/go/repositories.bzl b/third_party/go/repositories.bzl
index 5540762..b16e523 100644
--- a/third_party/go/repositories.bzl
+++ b/third_party/go/repositories.bzl
@@ -3520,6 +3520,13 @@
         version = "v1.2.0",
     )
     go_repository(
+        name = "com_github_mdlayher_kobject",
+        importpath = "github.com/mdlayher/kobject",
+        sum = "h1:JmrZTpS0GAyMV4ZQVVH/AS0Y6r2PbnYNSRUuRX+HOLA=",
+        version = "v0.0.0-20200520190114-19ca17470d7d",
+    )
+
+    go_repository(
         name = "com_github_mdlayher_netlink",
         importpath = "github.com/mdlayher/netlink",
         sum = "h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=",