inventory: add titan-16 and stage rpi5/jetson classes

This commit is contained in:
Brad Stein 2026-04-01 11:09:34 -03:00
parent 29a546179c
commit 34dfc165d6
3 changed files with 326 additions and 3 deletions

View File

@ -19,8 +19,127 @@ classes:
hardware: rpi4 hardware: rpi4
node-role.kubernetes.io/worker: "true" node-role.kubernetes.io/worker: "true"
root_overlay: overlays/rpi4-armbian-longhorn-root 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: 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 - name: titan-12
class: rpi4-armbian-worker class: rpi4-armbian-worker
hostname: titan-12 hostname: titan-12
@ -31,6 +150,16 @@ nodes:
ssh_user: atlas ssh_user: atlas
ssh_authorized_keys: ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion - 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 - name: titan-13
class: rpi4-armbian-longhorn class: rpi4-armbian-longhorn
hostname: titan-13 hostname: titan-13
@ -119,3 +248,73 @@ nodes:
ssh_user: atlas ssh_user: atlas
ssh_authorized_keys: ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion - 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

View File

@ -187,7 +187,7 @@ func (a *App) State(deviceHost string) PageState {
DefaultFlashHost: a.settings.DefaultFlashHost, DefaultFlashHost: a.settings.DefaultFlashHost,
SelectedHost: deviceHost, SelectedHost: deviceHost,
FlashHosts: flashHosts, FlashHosts: flashHosts,
Nodes: append([]inventory.NodeSpec{}, a.inventory.Nodes...), Nodes: a.replacementNodes(),
Jobs: jobs, Jobs: jobs,
Devices: devices, Devices: devices,
PreferredDevice: preferredDevice, PreferredDevice: preferredDevice,
@ -201,7 +201,7 @@ func (a *App) State(deviceHost string) PageState {
// Build starts a background image build for a node. // Build starts a background image build for a node.
func (a *App) Build(node string) (*Job, error) { 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 return nil, err
} }
job := a.newJob("build", node, "", "") job := a.newJob("build", node, "", "")
@ -214,7 +214,7 @@ func (a *App) Replace(node, host, device string) (*Job, error) {
if host == "" { if host == "" {
host = a.settings.DefaultFlashHost host = a.settings.DefaultFlashHost
} }
if _, _, err := a.inventory.FindNode(node); err != nil { if err := a.ensureReplacementReady(node); err != nil {
return nil, err return nil, err
} }
if _, err := a.ensureDevice(host, device); err != nil { if _, err := a.ensureDevice(host, device); err != nil {
@ -405,6 +405,59 @@ func cachedImageName(source string) string {
return strings.TrimSuffix(filepath.Base(source), ".xz") 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 { func (a *App) flashHosts() []string {
hosts := map[string]struct{}{} hosts := map[string]struct{}{}
for _, host := range a.settings.FlashHosts { for _, host := range a.settings.FlashHosts {

View File

@ -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) { func TestInternalSnapshotAndWatch(t *testing.T) {
app := newTestApp(t) app := newTestApp(t)
handler := app.Handler() handler := app.Handler()
@ -163,6 +232,8 @@ nodes:
k3s_url: https://192.168.22.7:6443 k3s_url: https://192.168.22.7:6443
k3s_token: token k3s_token: token
ssh_user: atlas ssh_user: atlas
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion
` `
if err := os.WriteFile(inventoryPath, []byte(inv), 0o644); err != nil { if err := os.WriteFile(inventoryPath, []byte(inv), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)