From 34dfc165d645055544b34049d2a92756c0bbb149 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 1 Apr 2026 11:09:34 -0300 Subject: [PATCH] inventory: add titan-16 and stage rpi5/jetson classes --- inventory.titan-rpi4.yaml | 199 +++++++++++++++++++++++++++++++++++++ pkg/service/app.go | 59 ++++++++++- pkg/service/server_test.go | 71 +++++++++++++ 3 files changed, 326 insertions(+), 3 deletions(-) diff --git a/inventory.titan-rpi4.yaml b/inventory.titan-rpi4.yaml index d52ff10..8c0d389 100644 --- a/inventory.titan-rpi4.yaml +++ b/inventory.titan-rpi4.yaml @@ -19,8 +19,127 @@ classes: hardware: rpi4 node-role.kubernetes.io/worker: "true" root_overlay: overlays/rpi4-armbian-longhorn-root + - name: rpi5-ubuntu-worker + arch: arm64 + os: ubuntu-24.04-raspi + image: ${METIS_IMAGE_RPI5_UBUNTU_WORKER} + checksum: ${METIS_IMAGE_RPI5_UBUNTU_WORKER_SHA256} + k3s_version: v1.31.5+k3s1 + default_labels: + hardware: rpi5 + node-role.kubernetes.io/worker: "true" + - name: jetson-ubuntu-accelerator + arch: arm64 + os: ubuntu-20.04-tegra + image: ${METIS_IMAGE_JETSON_UBUNTU_ACCELERATOR} + checksum: ${METIS_IMAGE_JETSON_UBUNTU_ACCELERATOR_SHA256} + k3s_version: v1.31.5+k3s1 + default_labels: + hardware: jetson + jetson: "true" + node-role.kubernetes.io/accelerator: "true" + - name: rpi5-ubuntu-control-plane + arch: arm64 + os: ubuntu-24.04-raspi + image: ${METIS_IMAGE_RPI5_UBUNTU_CONTROL} + checksum: ${METIS_IMAGE_RPI5_UBUNTU_CONTROL_SHA256} + k3s_version: v1.31.5+k3s1 + default_labels: + hardware: rpi5 + node-role.kubernetes.io/control-plane: "true" + default_taints: + - node-role.kubernetes.io/control-plane:NoSchedule + - name: amd64-debian-worker + arch: amd64 + os: debian-13 + image: ${METIS_IMAGE_AMD64_DEBIAN_WORKER} + checksum: ${METIS_IMAGE_AMD64_DEBIAN_WORKER_SHA256} + k3s_version: v1.31.5+k3s1 + default_labels: + hardware: amd64 + node-role.kubernetes.io/worker: "true" nodes: + - name: titan-04 + class: rpi5-ubuntu-worker + hostname: titan-04 + ip: 192.168.22.30 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-05 + class: rpi5-ubuntu-worker + hostname: titan-05 + ip: 192.168.22.31 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-06 + class: rpi5-ubuntu-worker + hostname: titan-06 + ip: 192.168.22.32 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-07 + class: rpi5-ubuntu-worker + hostname: titan-07 + ip: 192.168.22.33 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-08 + class: rpi5-ubuntu-worker + hostname: titan-08 + ip: 192.168.22.34 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-09 + class: rpi5-ubuntu-worker + hostname: titan-09 + ip: 192.168.22.35 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-10 + class: rpi5-ubuntu-worker + hostname: titan-10 + ip: 192.168.22.36 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-11 + class: rpi5-ubuntu-worker + hostname: titan-11 + ip: 192.168.22.37 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion - name: titan-12 class: rpi4-armbian-worker hostname: titan-12 @@ -31,6 +150,16 @@ nodes: ssh_user: atlas ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-16 + class: rpi4-armbian-worker + hostname: titan-16 + ip: 192.168.22.44 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion - name: titan-13 class: rpi4-armbian-longhorn hostname: titan-13 @@ -119,3 +248,73 @@ nodes: ssh_user: atlas ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-20 + class: jetson-ubuntu-accelerator + hostname: titan-20 + ip: 192.168.22.20 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-21 + class: jetson-ubuntu-accelerator + hostname: titan-21 + ip: 192.168.22.21 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-22 + class: amd64-debian-worker + hostname: titan-22 + ip: 192.168.22.22 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-24 + class: amd64-debian-worker + hostname: titan-24 + ip: 192.168.22.26 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-0a + class: rpi5-ubuntu-control-plane + hostname: titan-0a + ip: 192.168.22.11 + k3s_role: server + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-0b + class: rpi5-ubuntu-control-plane + hostname: titan-0b + ip: 192.168.22.12 + k3s_role: server + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-0c + class: rpi5-ubuntu-control-plane + hostname: titan-0c + ip: 192.168.22.13 + k3s_role: server + k3s_url: https://192.168.22.7:6443 + k3s_token: ${METIS_K3S_TOKEN} + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion diff --git a/pkg/service/app.go b/pkg/service/app.go index 9e94804..f7518d2 100644 --- a/pkg/service/app.go +++ b/pkg/service/app.go @@ -187,7 +187,7 @@ func (a *App) State(deviceHost string) PageState { DefaultFlashHost: a.settings.DefaultFlashHost, SelectedHost: deviceHost, FlashHosts: flashHosts, - Nodes: append([]inventory.NodeSpec{}, a.inventory.Nodes...), + Nodes: a.replacementNodes(), Jobs: jobs, Devices: devices, PreferredDevice: preferredDevice, @@ -201,7 +201,7 @@ func (a *App) State(deviceHost string) PageState { // Build starts a background image build for a node. func (a *App) Build(node string) (*Job, error) { - if _, _, err := a.inventory.FindNode(node); err != nil { + if err := a.ensureReplacementReady(node); err != nil { return nil, err } job := a.newJob("build", node, "", "") @@ -214,7 +214,7 @@ func (a *App) Replace(node, host, device string) (*Job, error) { if host == "" { host = a.settings.DefaultFlashHost } - if _, _, err := a.inventory.FindNode(node); err != nil { + if err := a.ensureReplacementReady(node); err != nil { return nil, err } if _, err := a.ensureDevice(host, device); err != nil { @@ -405,6 +405,59 @@ func cachedImageName(source string) string { return strings.TrimSuffix(filepath.Base(source), ".xz") } +func (a *App) replacementNodes() []inventory.NodeSpec { + nodes := make([]inventory.NodeSpec, 0, len(a.inventory.Nodes)) + for _, node := range a.inventory.Nodes { + spec, class, err := a.inventory.FindNode(node.Name) + if err != nil { + continue + } + if replacementReady(spec, class) { + nodes = append(nodes, node) + } + } + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Name < nodes[j].Name + }) + return nodes +} + +func (a *App) ensureReplacementReady(nodeName string) error { + node, class, err := a.inventory.FindNode(nodeName) + if err != nil { + return err + } + if replacementReady(node, class) { + return nil + } + return fmt.Errorf("node %s does not yet have a complete replacement definition", nodeName) +} + +func replacementReady(node *inventory.NodeSpec, class *inventory.NodeClass) bool { + if node == nil || class == nil { + return false + } + if strings.TrimSpace(class.Image) == "" || strings.TrimSpace(class.Checksum) == "" { + return false + } + if strings.TrimSpace(node.Name) == "" || strings.TrimSpace(node.Hostname) == "" || strings.TrimSpace(node.IP) == "" { + return false + } + if strings.TrimSpace(node.K3sRole) == "" { + return false + } + if strings.TrimSpace(node.K3sRole) != "server" && strings.TrimSpace(node.K3sURL) == "" { + return false + } + if strings.TrimSpace(node.K3sToken) == "" { + return false + } + if strings.TrimSpace(node.SSHUser) == "" || len(node.SSHAuthorized) == 0 { + return false + } + return true +} + func (a *App) flashHosts() []string { hosts := map[string]struct{}{} for _, host := range a.settings.FlashHosts { diff --git a/pkg/service/server_test.go b/pkg/service/server_test.go index 2d154d9..57c6e0e 100644 --- a/pkg/service/server_test.go +++ b/pkg/service/server_test.go @@ -81,6 +81,75 @@ func TestStateJSONUsesLowerCaseNodeFields(t *testing.T) { } } +func TestStateFiltersNodesWithoutCompleteReplacementDefinition(t *testing.T) { + dir := t.TempDir() + baseImage := filepath.Join(dir, "base.img") + if err := os.WriteFile(baseImage, []byte("test-image"), 0o644); err != nil { + t.Fatal(err) + } + sum := sha256.Sum256([]byte("test-image")) + inventoryPath := filepath.Join(dir, "inventory.yaml") + inv := ` +classes: + - name: ready + arch: arm64 + os: armbian + image: file://` + baseImage + ` + checksum: sha256:` + hex.EncodeToString(sum[:]) + ` + k3s_version: v1.31.5+k3s1 + - name: incomplete + arch: arm64 + os: ubuntu + image: file://` + baseImage + ` +nodes: + - name: titan-ready + class: ready + hostname: titan-ready + ip: 192.168.22.240 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: token + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion + - name: titan-incomplete + class: incomplete + hostname: titan-incomplete + ip: 192.168.22.241 + k3s_role: agent + k3s_url: https://192.168.22.7:6443 + k3s_token: token + ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion +` + if err := os.WriteFile(inventoryPath, []byte(inv), 0o644); err != nil { + t.Fatal(err) + } + settings := Settings{ + BindAddr: ":0", + InventoryPath: inventoryPath, + CacheDir: filepath.Join(dir, "cache"), + ArtifactDir: filepath.Join(dir, "artifacts"), + HistoryPath: filepath.Join(dir, "history.jsonl"), + SnapshotsPath: filepath.Join(dir, "snapshots.json"), + TargetsPath: filepath.Join(dir, "targets.json"), + DefaultFlashHost: "titan-22", + FlashHosts: []string{"titan-22"}, + LocalHost: "titan-22", + AllowedGroups: []string{"admin", "maintenance"}, + MaxDeviceBytes: 300000000000, + } + app, err := NewApp(settings) + if err != nil { + t.Fatalf("new app: %v", err) + } + state := app.State("titan-22") + if len(state.Nodes) != 1 || state.Nodes[0].Name != "titan-ready" { + t.Fatalf("expected only titan-ready in state nodes, got %+v", state.Nodes) + } +} + func TestInternalSnapshotAndWatch(t *testing.T) { app := newTestApp(t) handler := app.Handler() @@ -163,6 +232,8 @@ nodes: k3s_url: https://192.168.22.7:6443 k3s_token: token ssh_user: atlas + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion ` if err := os.WriteFile(inventoryPath, []byte(inv), 0o644); err != nil { t.Fatal(err)