From c3032683ab088fa2ac13022e1356503432cf25ef Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Fri, 30 Oct 2026 22:07:22 +0100 Subject: [PATCH] Device sync: v6 primary IP, DMI placeholders, CLI report The primary IP of each family is the management address, else the first address on the management interface (the v6 address next to a v4 management address). DMI placeholders ("Not Specified", "To Be Filled By O.E.M.") are not written as serials. rollcall ipam sync prints the device report. Refs #10 Co-Authored-By: Claude Opus 5.5 --- cmd/rollcall/nbdevices.go | 32 +++++++++++++++++++++++++++----- cmd/rollcall/nbdevices_test.go | 15 ++++++++++++++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/cmd/rollcall/nbdevices.go b/cmd/rollcall/nbdevices.go index 25d1ff2..58887c9 100644 --- a/cmd/rollcall/nbdevices.go +++ b/cmd/rollcall/nbdevices.go @@ -232,7 +232,7 @@ func (n *netBox) syncDevices(ctx context.Context, cfg DeviceSyncConfig, machines site = id } body := obj{"name": name, "device_type": types[tk], "role": roleID, "site": site, "status": nbStatus(m.State), - "serial": trunc(deref(m.Serial), 50), "tags": tags, "custom_fields": obj{n.field: m.ID}, + "serial": trunc(dmiValue(deref(m.Serial)), 50), "tags": tags, "custom_fields": obj{n.field: m.ID}, "location": nil, "rack": nil, "position": nil, "face": nil} if id, ok := locBy[siteName{site, strings.ToLower(m.Location.Room)}]; ok && m.Location.Room != "" { body["location"] = id @@ -376,14 +376,21 @@ func (n *netBox) syncInterfaces(ctx context.Context, dev obj, m *Machine, ips [] } } } - // addresses on their interfaces; the management address (else the first - // one on the boot NIC) is the primary IP + // addresses on their interfaces; the primary IP of each family is the + // management address, else the first address on the management (or + // boot) interface var boot string if b := bootNIC(nics, m.MACs); b != nil { boot = b.Name } mgmt := deref(m.MgmtIP) + type cand struct { + id, iface int64 + fam int + } + var cands []cand primary := map[int]int64{} + mgmtIface := ifaceID[boot] for _, x := range ips { a, ok := nbIPs[x.IP] if !ok || n.mine(a) != m.ID { @@ -405,8 +412,14 @@ func (n *netBox) syncInterfaces(ctx context.Context, dev obj, m *Machine, ips [] if strings.Contains(x.IP, ":") { fam = 6 } - if _, set := primary[fam]; x.IP == mgmt || (!set && iid == ifaceID[boot] && mgmt == "") { - primary[fam] = nbID(a) + if x.IP == mgmt { + primary[fam], mgmtIface = nbID(a), iid + } + cands = append(cands, cand{nbID(a), iid, fam}) + } + for _, c := range cands { + if _, set := primary[c.fam]; !set && c.iface == mgmtIface { + primary[c.fam] = c.id } } body := obj{} @@ -441,3 +454,12 @@ func nbIPFromObj(a obj) nbIP { } return x } + +// dmiValue drops the placeholders firmware leaves in DMI fields. +func dmiValue(s string) string { + switch strings.ToLower(strings.TrimSpace(s)) { + case "", "not specified", "to be filled by o.e.m.", "default string", "system serial number", "0123456789", "none", "n/a": + return "" + } + return strings.TrimSpace(s) +} diff --git a/cmd/rollcall/nbdevices_test.go b/cmd/rollcall/nbdevices_test.go index db4de8c..2dc87cf 100644 --- a/cmd/rollcall/nbdevices_test.go +++ b/cmd/rollcall/nbdevices_test.go @@ -231,6 +231,8 @@ func TestNetBoxDeviceSync(t *testing.T) { tagged := []any{map[string]any{"name": "rollcall"}} ipID := f.put("ipam/ip-addresses/", map[string]any{"address": "10.213.77.50/24", "tags": tagged, "custom_fields": map[string]any{"rollcall_machine": "m-a"}}) + ip6ID := f.put("ipam/ip-addresses/", map[string]any{"address": "fd00:213:77::50/64", "tags": tagged, + "custom_fields": map[string]any{"rollcall_machine": "m-a"}}) foreignIP := f.put("ipam/ip-addresses/", map[string]any{"address": "10.213.77.51/24", "tags": []any{}, "custom_fields": map[string]any{}}) // a device someone else made with a machine's name @@ -241,7 +243,12 @@ func TestNetBoxDeviceSync(t *testing.T) { b := dcimMachine("m-b", "", "ready", Location{Building: "nowhere", Rack: "R12"}, [2]string{"eth0", "aa:bb:cc:00:00:03"}) c := dcimMachine("m-c", "taken", "commission-failed", Location{}) n := dcimMachine("m-n", "", "new", Location{}) - ips := []IPAddr{{IP: "10.213.77.50", MachineID: "m-a", Iface: "eno1"}, {IP: "10.213.77.51", MachineID: "m-b"}} + ips := []IPAddr{{IP: "10.213.77.50", MachineID: "m-a", Iface: "eno1"}, {IP: "fd00:213:77::50", MachineID: "m-a", Iface: "eno1"}, + {IP: "10.213.77.51", MachineID: "m-b"}} + mgmt := "10.213.77.50" + a.MgmtIP = &mgmt + ns := "Not Specified" + b.Serial = &ns rep := nb.syncDevices(ctx, cfg, []*Machine{a, b, c, n}, ips) if rep.Error != "" { @@ -283,6 +290,12 @@ func TestNetBoxDeviceSync(t *testing.T) { if ip["assigned_object_type"] != "dcim.interface" || ip["assigned_object_id"] != eno1["id"] || d["primary_ip4"] != float64(ipID) { t.Fatalf("ip %v primary %v", ip, d["primary_ip4"]) } + if d["primary_ip6"] != float64(ip6ID) { + t.Fatalf("primary ip6 %v (the v6 address on the management interface)", d["primary_ip6"]) + } + if db["serial"] != "" { + t.Fatalf("DMI placeholder as serial: %v", db["serial"]) + } if f.objs["ipam/ip-addresses/"][foreignIP]["assigned_object_id"] != nil { t.Fatal("assigned an address that is not Rollcall's") }