fix: chassis names mismatch

This commit is contained in:
Julian Sommer
2026-03-27 10:26:16 +10:00
parent 216842cc1b
commit cbc04a2437
4 changed files with 138 additions and 10 deletions
+3 -1
View File
@@ -711,7 +711,9 @@ var vpcdStartCmd = &cobra.Command{
})
}
// Derive OVN chassis names from cluster node names (format: "chassis-{hostname}")
// Fallback chassis names derived from config node names. vpcd prefers
// discovering chassis from the OVN Southbound DB at startup; these are
// only used if that discovery fails.
var chassisNames []string
for nodeName := range clusterConfig.Nodes {
chassisNames = append(chassisNames, "chassis-"+nodeName)
+3 -4
View File
@@ -160,10 +160,9 @@ elif [ -n "$WAN_IFACE" ]; then
fi
fi
# Chassis ID must match what vpcd derives from the node name in spinifex.toml.
# vpcd generates "chassis-{nodeName}" (e.g., chassis-node1) and uses it to schedule
# gateway chassis in the NB DB. ovn-controller registers in the SB DB using the OVS
# system-id. If they don't match, gateway traffic has no host to schedule to.
# Chassis ID — vpcd discovers registered chassis from the OVN Southbound DB
# at startup, so the name here doesn't need to match spinifex.toml node names.
# Using "chassis-node1" for consistency with the default node name.
NODE_NAME="node1"
CHASSIS_ID="chassis-${NODE_NAME}"
+37 -5
View File
@@ -56,8 +56,9 @@ type Config struct {
ExternalMode string
// ExternalPools holds the cluster-wide external IP pool configs.
ExternalPools []ExternalPoolConfig
// ChassisNames are the OVN chassis identifiers for gateway HA scheduling.
// Format: "chassis-{hostname}" — one per node in the cluster.
// ChassisNames are fallback OVN chassis identifiers for gateway HA scheduling.
// Normally discovered from the OVN Southbound DB at startup. Only used if
// SBDB discovery fails.
ChassisNames []string
// Bootstrap holds the default VPC config from spinifex.toml for first-boot reconciliation.
Bootstrap *BootstrapVPC
@@ -181,6 +182,30 @@ var checkOVNController = func() error {
return fmt.Errorf("ovn-controller is not running: run ./scripts/setup-ovn.sh --management")
}
// discoverChassis queries the OVN Southbound DB for registered chassis names.
// This is the source of truth — whatever names setup-ovn.sh registered are what
// we use for gateway scheduling. Eliminates the brittle coupling between
// spinifex.toml node names and the OVS system-id set during OVN setup.
var discoverChassis = func(sbAddr string) ([]string, error) {
args := []string{"--no-leader-only"}
if sbAddr != "" {
args = append(args, "--db="+sbAddr)
}
args = append(args, "list-chassis")
out, err := sudoCommand("ovn-sbctl", args...).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("ovn-sbctl list-chassis: %s: %w", strings.TrimSpace(string(out)), err)
}
var names []string
for line := range strings.SplitSeq(strings.TrimSpace(string(out)), "\n") {
name := strings.TrimSpace(line)
if name != "" {
names = append(names, name)
}
}
return names, nil
}
// preflightOVN runs all OVN preflight checks and returns the first failure.
func preflightOVN() error {
if err := checkBrInt(); err != nil {
@@ -250,9 +275,16 @@ func launchService(cfg *Config) error {
topoOpts = append(topoOpts, WithExternalNetwork(cfg.ExternalMode, cfg.ExternalPools))
slog.Info("External network enabled", "mode", cfg.ExternalMode, "pools", len(cfg.ExternalPools))
}
if len(cfg.ChassisNames) > 0 {
topoOpts = append(topoOpts, WithChassisNames(cfg.ChassisNames))
slog.Info("Gateway chassis configured", "chassis", cfg.ChassisNames)
// Discover chassis from OVN SBDB (source of truth). Fall back to config
// if SBDB query fails (e.g., SBDB address not configured).
chassisNames, err := discoverChassis(cfg.OVNSBAddr)
if err != nil {
slog.Warn("Failed to discover chassis from OVN SBDB, falling back to config", "err", err)
chassisNames = cfg.ChassisNames
}
if len(chassisNames) > 0 {
topoOpts = append(topoOpts, WithChassisNames(chassisNames))
slog.Info("Gateway chassis configured", "source", "sbdb", "chassis", chassisNames)
}
topoOpts = append(topoOpts, WithBridgeMode(bridgeMode))
topo := NewTopologyHandler(liveClient, topoOpts...)
+95
View File
@@ -66,6 +66,101 @@ func TestPreflightOVN_ControllerNotRunning(t *testing.T) {
}
}
func TestDiscoverChassis_ParsesOutput(t *testing.T) {
orig := discoverChassis
defer func() { discoverChassis = orig }()
discoverChassis = func(sbAddr string) ([]string, error) {
// Simulate ovn-sbctl list-chassis output
output := "chassis-node1\nchassis-node2\nchassis-node3\n"
var names []string
for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
name := strings.TrimSpace(line)
if name != "" {
names = append(names, name)
}
}
return names, nil
}
names, err := discoverChassis("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(names) != 3 {
t.Fatalf("expected 3 chassis, got %d: %v", len(names), names)
}
expected := map[string]bool{"chassis-node1": true, "chassis-node2": true, "chassis-node3": true}
for _, n := range names {
if !expected[n] {
t.Errorf("unexpected chassis name: %s", n)
}
}
}
func TestDiscoverChassis_SingleNode(t *testing.T) {
orig := discoverChassis
defer func() { discoverChassis = orig }()
discoverChassis = func(sbAddr string) ([]string, error) {
output := "chassis-spinifex-image-builder\n"
var names []string
for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
name := strings.TrimSpace(line)
if name != "" {
names = append(names, name)
}
}
return names, nil
}
names, err := discoverChassis("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(names) != 1 || names[0] != "chassis-spinifex-image-builder" {
t.Errorf("expected [chassis-spinifex-image-builder], got %v", names)
}
}
func TestDiscoverChassis_EmptyOutput(t *testing.T) {
orig := discoverChassis
defer func() { discoverChassis = orig }()
discoverChassis = func(sbAddr string) ([]string, error) {
return nil, nil
}
names, err := discoverChassis("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(names) != 0 {
t.Errorf("expected empty chassis list, got %v", names)
}
}
func TestDiscoverChassis_Error_FallsBackToConfig(t *testing.T) {
orig := discoverChassis
defer func() { discoverChassis = orig }()
discoverChassis = func(sbAddr string) ([]string, error) {
return nil, fmt.Errorf("connection refused")
}
// Simulate the fallback logic from launchService
_, err := discoverChassis("")
if err == nil {
t.Fatal("expected error from discoverChassis")
}
// Fallback to config (as launchService does)
chassisNames := []string{"chassis-node1"}
if len(chassisNames) != 1 || chassisNames[0] != "chassis-node1" {
t.Errorf("expected fallback to config names, got %v", chassisNames)
}
}
func TestPreflightOVN_BothFail_ReportsFirst(t *testing.T) {
origBrInt := checkBrInt
origCtrl := checkOVNController