diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index 849daba93..a25861bdd 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -843,13 +843,13 @@ func runAdminInit(cmd *cobra.Command, args []string) { OVNNBAddr: "tcp:127.0.0.1:6641", OVNSBAddr: "tcp:127.0.0.1:6642", - ExternalMode: externalMode, - ExternalIface: externalIface, - WanBridge: detectedWanBridge(detectedNet), - ExternalDHCP: useExternalDHCP, - PoolName: "wan", - PoolStart: poolStart, - PoolEnd: poolEnd, + ExternalMode: externalMode, + ExternalIface: externalIface, + WanBridge: detectedWanBridge(detectedNet), + ExternalDHCP: useExternalDHCP, + PoolName: "wan", + PoolStart: poolStart, + PoolEnd: poolEnd, PoolGateway: externalGateway, PoolGatewayIP: gatewayIP, PoolPrefixLen: externalPrefixLen, @@ -1704,18 +1704,23 @@ func writeBootstrapFilesWithAdmin(configDir string, masterKey []byte, accessKey, return handlers_iam.SaveBootstrapData(filepath.Join(configDir, "bootstrap.json"), bd) } -// detectDNSServers reads the DNS servers configured on the host for the given -// detectedWanBridge returns the WAN bridge name from the detected network -// topology. If the WAN interface is already a bridge, returns its name. -// Otherwise returns "br-wan" as the default. +// detectedWanBridge returns the OVS bridge name that OVN bridge-mappings should +// use for WAN traffic. If the WAN interface is already an OVS bridge, returns +// its name directly. If it's a Linux bridge, returns "br-ext" — setup-ovn.sh +// will create this OVS bridge and link it to the Linux bridge via a veth pair. +// If it's a physical NIC, returns "br-wan" as the default. func detectedWanBridge(detected *admin.DetectedNetwork) string { if detected == nil || detected.WAN == nil { return "" } - // Check if the WAN interface name looks like a bridge (starts with "br-") - // If so, it's already set up — use it directly. - if strings.HasPrefix(detected.WAN.Name, "br-") { - return detected.WAN.Name + name := detected.WAN.Name + if strings.HasPrefix(name, "br-") { + // Check if it's an OVS bridge (use directly) or Linux bridge (need br-ext) + if out, err := exec.Command("sudo", "ovs-vsctl", "br-exists", name).CombinedOutput(); err == nil && len(out) == 0 { + return name // OVS bridge — use directly + } + // Linux bridge — setup-ovn.sh creates br-ext + veth pair + return "br-ext" } return "br-wan" } @@ -1748,7 +1753,7 @@ func detectDNSServers(iface string) []string { data, err := os.ReadFile("/etc/resolv.conf") if err == nil { var servers []string - for _, line := range strings.Split(string(data), "\n") { + for line := range strings.SplitSeq(string(data), "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "nameserver ") { ip := strings.TrimSpace(strings.TrimPrefix(line, "nameserver")) @@ -1774,14 +1779,14 @@ func detectDNSServers(iface string) []string { // Format: "Link 2 (enp0s3): 192.168.1.1 8.8.8.8 1.1.1.1" func parseDNSFromResolvectl(output string) []string { var servers []string - for _, line := range strings.Split(output, "\n") { + for line := range strings.SplitSeq(output, "\n") { // Find the colon separator, IPs come after it - idx := strings.Index(line, ":") - if idx < 0 { + _, after, ok := strings.Cut(line, ":") + if !ok { continue } - fields := strings.Fields(line[idx+1:]) - for _, f := range fields { + fields := strings.FieldsSeq(after) + for f := range fields { if net.ParseIP(f) != nil && f != "127.0.0.53" && f != "127.0.0.1" { servers = append(servers, f) } diff --git a/scripts/reset-dev-env.sh b/scripts/reset-dev-env.sh index 6efbca38f..5bece6c74 100755 --- a/scripts/reset-dev-env.sh +++ b/scripts/reset-dev-env.sh @@ -101,6 +101,12 @@ else echo " /var/lib/ovn not found, skipping OVN DB cleanup" fi +# Remove veth pair created by setup-ovn.sh (veth mode — Linux bridge ↔ OVS bridge) +if ip link show veth-wan-br >/dev/null 2>&1; then + echo " Deleting veth pair: veth-wan-br ↔ veth-wan-ovs" + sudo ip link del veth-wan-br 2>/dev/null || true +fi + # Remove macvlan interfaces created by setup-ovn.sh for iface in $(ip -o link show type macvlan 2>/dev/null | awk -F': ' '{print $2}' | grep '^spx-ext-'); do echo " Deleting macvlan: $iface" diff --git a/scripts/setup-ovn.sh b/scripts/setup-ovn.sh index fbbd5f037..92b8c3ca2 100755 --- a/scripts/setup-ovn.sh +++ b/scripts/setup-ovn.sh @@ -26,7 +26,9 @@ # # WAN Bridge Auto-Detection: # When no --wan-bridge is given, the script checks the default route interface: -# - If it's already a bridge (e.g. br-wan from cloud-init) → use it directly +# - If it's an OVS bridge → use it directly for bridge-mappings +# - If it's a Linux bridge → create OVS br-ext + veth pair to link them +# (non-destructive, Linux bridge keeps IP/routes, no interruption) # - If it's a physical NIC → stop and print guidance (cannot safely move NIC) # # Examples: @@ -81,7 +83,8 @@ done # --- WAN bridge auto-detection --- # Determine the WAN bridge name and how to set it up. -WAN_BRIDGE_MODE="" # "existing", "direct", "macvlan", or "" +WAN_BRIDGE_MODE="" # "existing", "veth", "direct", "macvlan", or "" +LINUX_BRIDGE="" # Set when WAN_BRIDGE_MODE="veth" — the Linux bridge behind the veth pair detect_wan_bridge() { # If --wan-bridge was explicitly given, use it @@ -130,10 +133,19 @@ detect_wan_bridge() { fi if [ "$is_bridge" = true ]; then - # Default route goes through a bridge — use it directly - WAN_BRIDGE="$default_dev" - WAN_BRIDGE_MODE="existing" - echo " Auto-detected WAN bridge: $WAN_BRIDGE (default route interface)" + if sudo ovs-vsctl br-exists "$default_dev" 2>/dev/null; then + # Already an OVS bridge — use it directly for bridge-mappings + WAN_BRIDGE="$default_dev" + WAN_BRIDGE_MODE="existing" + echo " Auto-detected WAN bridge: $WAN_BRIDGE (OVS bridge, default route)" + else + # Linux bridge — link to OVS via veth pair (non-destructive) + LINUX_BRIDGE="$default_dev" + WAN_BRIDGE="br-ext" + WAN_BRIDGE_MODE="veth" + echo " Auto-detected Linux bridge: $LINUX_BRIDGE (default route)" + echo " Will create OVS bridge br-ext + veth pair to link them" + fi return fi @@ -196,6 +208,9 @@ echo "=== Spinifex OVN Compute Node Setup ===" echo " Management node: $MANAGEMENT" if [ -n "$WAN_BRIDGE" ]; then echo " WAN bridge: $WAN_BRIDGE ($WAN_BRIDGE_MODE)" + if [ -n "$LINUX_BRIDGE" ]; then + echo " Linux bridge: $LINUX_BRIDGE (linked via veth pair)" + fi if [ -n "$WAN_IFACE" ]; then echo " WAN interface: $WAN_IFACE" fi @@ -280,16 +295,56 @@ if [ -n "$WAN_BRIDGE" ]; then case "$WAN_BRIDGE_MODE" in existing) - # Bridge already exists (e.g. br-wan from cloud-init). - # Ensure it's registered as an OVS bridge. If it's a Linux bridge - # that OVS doesn't know about, create a new OVS bridge with the - # same name — OVS will take over. + # Already an OVS bridge (from a previous run or explicit --wan-bridge). if ! sudo ovs-vsctl br-exists "$WAN_BRIDGE" 2>/dev/null; then sudo ovs-vsctl --may-exist add-br "$WAN_BRIDGE" echo " created OVS bridge: $WAN_BRIDGE" fi sudo ip link set "$WAN_BRIDGE" up - echo " $WAN_BRIDGE: existing bridge, up" + echo " $WAN_BRIDGE: OVS bridge, up" + ;; + + veth) + # Linux bridge detected (e.g. br-wan from cloud-init/netplan). + # OVN bridge-mappings require an OVS bridge. Rather than converting + # the Linux bridge (destructive, causes WAN interruption), we create + # a separate OVS bridge and link them with a veth pair: + # + # br-wan (Linux, keeps IP/routes) ←→ veth pair ←→ br-ext (OVS, for OVN) + # + # No network interruption. The Linux bridge is untouched. + + # Create OVS bridge + if ! sudo ovs-vsctl br-exists "$WAN_BRIDGE" 2>/dev/null; then + sudo ovs-vsctl --may-exist add-br "$WAN_BRIDGE" + echo " created OVS bridge: $WAN_BRIDGE" + fi + sudo ip link set "$WAN_BRIDGE" up + + # Create veth pair (idempotent) + if ! ip link show veth-wan-br >/dev/null 2>&1; then + sudo ip link add veth-wan-br type veth peer name veth-wan-ovs + echo " created veth pair: veth-wan-br ↔ veth-wan-ovs" + else + echo " veth pair already exists: veth-wan-br ↔ veth-wan-ovs" + fi + + # Enslave veth-wan-br to the Linux bridge + if ! ip link show veth-wan-br 2>/dev/null | grep -q "master $LINUX_BRIDGE"; then + sudo ip link set veth-wan-br master "$LINUX_BRIDGE" + echo " veth-wan-br → $LINUX_BRIDGE (Linux bridge)" + fi + sudo ip link set veth-wan-br up + + # Add veth-wan-ovs to the OVS bridge + if ! sudo ovs-vsctl port-to-br veth-wan-ovs >/dev/null 2>&1; then + sudo ovs-vsctl --may-exist add-port "$WAN_BRIDGE" veth-wan-ovs + echo " veth-wan-ovs → $WAN_BRIDGE (OVS bridge)" + fi + sudo ip link set veth-wan-ovs up + + echo " $LINUX_BRIDGE (Linux) ↔ veth pair ↔ $WAN_BRIDGE (OVS)" + echo " $LINUX_BRIDGE keeps its IP and routes — no interruption" ;; direct) @@ -587,7 +642,15 @@ fi if [ -n "$WAN_BRIDGE" ]; then if sudo ovs-vsctl br-exists "$WAN_BRIDGE"; then echo " $WAN_BRIDGE:$(printf '%*s' $((15 - ${#WAN_BRIDGE})) '') OK" - if [ "$WAN_BRIDGE_MODE" = "direct" ]; then + if [ "$WAN_BRIDGE_MODE" = "veth" ]; then + if ip link show veth-wan-br >/dev/null 2>&1 && ip link show veth-wan-ovs >/dev/null 2>&1; then + echo " veth pair: OK (veth-wan-br ↔ veth-wan-ovs)" + echo " linux bridge: $LINUX_BRIDGE (untouched)" + else + echo " veth pair: FAILED (veth-wan-br/veth-wan-ovs not found)" + OK=false + fi + elif [ "$WAN_BRIDGE_MODE" = "direct" ]; then if sudo ovs-vsctl port-to-br "$WAN_IFACE" >/dev/null 2>&1; then echo " direct bridge: OK ($WAN_IFACE on $WAN_BRIDGE)" else diff --git a/scripts/stop-dev.sh b/scripts/stop-dev.sh index 65fd1b54a..21d654bca 100755 --- a/scripts/stop-dev.sh +++ b/scripts/stop-dev.sh @@ -150,9 +150,10 @@ has_service "predastore" && stop_service "predastore" "$PID_DIR" has_service "nats" && stop_service "nats" "$PID_DIR" # Stop OVN networking (prevents idle CPU burn when spinifex isn't running) -# CRITICAL: Do NOT stop openvswitch-switch if br-external has an IP. -# When setup-ovn.sh migrates the host's WAN IP to br-external, stopping OVS -# destroys the bridge and the host loses all WAN connectivity (unreachable via SSH). +# CRITICAL: Do NOT stop openvswitch-switch if OVS carries WAN traffic. +# This can happen when an OVS bridge has a WAN IP (direct bridge mode) or +# when a veth pair links an OVS bridge to a Linux WAN bridge (veth mode). +# Stopping OVS would destroy the bridge and break WAN connectivity. # OVS is infrastructure — only stop ovn-controller and ovn-central. if pidof systemd >/dev/null 2>&1; then echo "🛑 Stopping OVN networking..." @@ -161,9 +162,13 @@ if pidof systemd >/dev/null 2>&1; then sudo systemctl stop ovn-central 2>/dev/null && echo "✅ ovn-central stopped" || true fi - # Only stop OVS if the WAN bridge does NOT have an IP (safe to tear down). - # Check all OVS bridges — any bridge with a WAN IP means OVS is infrastructure. + # Only stop OVS if tearing it down won't kill WAN connectivity. + # Two cases where OVS is load-bearing for WAN: + # 1. An OVS bridge has a WAN IP directly (direct bridge mode) + # 2. A veth pair links a Linux bridge to an OVS bridge (veth mode) — + # stopping OVS destroys the OVS bridge end, breaking the link WAN_BRIDGE_HAS_IP=false + VETH_LINKS_WAN=false for br in $(sudo ovs-vsctl list-br 2>/dev/null); do if [ "$br" = "br-int" ]; then continue; fi if ip -4 addr show "$br" 2>/dev/null | grep -q "inet "; then @@ -171,8 +176,14 @@ if pidof systemd >/dev/null 2>&1; then break fi done + # Check if a veth pair links an OVS bridge to a Linux bridge with a WAN IP + if ip link show veth-wan-br >/dev/null 2>&1; then + VETH_LINKS_WAN=true + fi if [ "$WAN_BRIDGE_HAS_IP" = true ]; then echo "⚠️ Skipping openvswitch-switch stop ($br has WAN IP — stopping would kill connectivity)" + elif [ "$VETH_LINKS_WAN" = true ]; then + echo "⚠️ Skipping openvswitch-switch stop (veth pair links OVS to WAN bridge)" else sudo systemctl stop openvswitch-switch 2>/dev/null && echo "✅ openvswitch-switch stopped" || true fi diff --git a/spinifex/admin/admin.go b/spinifex/admin/admin.go index 10e0c8667..949bf6a3f 100644 --- a/spinifex/admin/admin.go +++ b/spinifex/admin/admin.go @@ -65,17 +65,17 @@ type ConfigSettings struct { OVNSBAddr string // External networking for public subnets - ExternalMode string // "pool", "nat", or "" (disabled) - ExternalIface string // WAN NIC name (e.g., "eth0", "eth1") - WanBridge string // OVS bridge for WAN traffic (default "br-wan") - ExternalDHCP bool // Obtain gateway IP via DHCP on macvlan/bridge - PoolName string // External pool name (e.g., "wan") - PoolStart string // First IP in external pool range - PoolEnd string // Last IP in external pool range - PoolGateway string // WAN gateway IP - PoolGatewayIP string // Explicit SNAT IP (for nat mode without DHCP) - PoolPrefixLen int // Subnet prefix length (default 24) - PoolDNSServers []string // DNS servers for VM DHCP (auto-detected from host) + ExternalMode string // "pool", "nat", or "" (disabled) + ExternalIface string // WAN NIC name (e.g., "eth0", "eth1") + WanBridge string // OVS bridge for WAN traffic (default "br-wan") + ExternalDHCP bool // Obtain gateway IP via DHCP on macvlan/bridge + PoolName string // External pool name (e.g., "wan") + PoolStart string // First IP in external pool range + PoolEnd string // Last IP in external pool range + PoolGateway string // WAN gateway IP + PoolGatewayIP string // Explicit SNAT IP (for nat mode without DHCP) + PoolPrefixLen int // Subnet prefix length (default 24) + PoolDNSServers []string // DNS servers for VM DHCP (auto-detected from host) // Other nodes in the cluster (for config source of truth) RemoteNodes []RemoteNode diff --git a/spinifex/admin/network_detect.go b/spinifex/admin/network_detect.go index df2604274..873d4d22d 100644 --- a/spinifex/admin/network_detect.go +++ b/spinifex/admin/network_detect.go @@ -62,7 +62,7 @@ func DetectNetwork() (*DetectedNetwork, error) { seen := make(map[string]bool) var interfaces []DetectedInterface - for _, line := range strings.Split(strings.TrimSpace(string(allRoutes)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(allRoutes)), "\n") { if line == "" { continue } @@ -169,10 +169,10 @@ func SuggestPoolRange(wan *DetectedInterface) (start, end string) { for i := 3; i >= 0 && carry > 0; i-- { val := int(startIP[i]) - carry if val < 0 { - startIP[i] = byte(256 + val) + startIP[i] = byte(256 + val) //nolint:gosec // val is in [-256,0), so 256+val is in [0,256) carry = 1 } else { - startIP[i] = byte(val) + startIP[i] = byte(val) //nolint:gosec // val is non-negative and <= 255 carry = 0 } } diff --git a/spinifex/config/config.go b/spinifex/config/config.go index fc851cb6e..2db8ed3f8 100644 --- a/spinifex/config/config.go +++ b/spinifex/config/config.go @@ -90,7 +90,7 @@ type VPCDConfig struct { OVNNBAddr string `mapstructure:"ovn_nb_addr"` // OVN Northbound DB address (e.g., "tcp:127.0.0.1:6641") OVNSBAddr string `mapstructure:"ovn_sb_addr"` // OVN Southbound DB address (e.g., "tcp:127.0.0.1:6642") ExternalInterface string `mapstructure:"external_interface"` // WAN NIC name (e.g., "eth1", "enp0s3") — the physical NIC on the WAN bridge - WanBridge string `mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network) + WanBridge string `mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network) BridgeMode string `mapstructure:"bridge_mode"` // "direct" or "macvlan" (auto-detected if empty) } diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go index 7915681da..132d43f3a 100644 --- a/spinifex/daemon/daemon.go +++ b/spinifex/daemon/daemon.go @@ -32,8 +32,8 @@ import ( "github.com/mulgadc/spinifex/spinifex/awserrors" "github.com/mulgadc/spinifex/spinifex/config" handlers_ec2_account "github.com/mulgadc/spinifex/spinifex/handlers/ec2/account" - handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip" handlers_ec2_eigw "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eigw" + handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip" handlers_ec2_igw "github.com/mulgadc/spinifex/spinifex/handlers/ec2/igw" handlers_ec2_image "github.com/mulgadc/spinifex/spinifex/handlers/ec2/image" handlers_ec2_instance "github.com/mulgadc/spinifex/spinifex/handlers/ec2/instance" diff --git a/spinifex/daemon/daemon_handlers_vpc.go b/spinifex/daemon/daemon_handlers_vpc.go index fdf552984..7d1c983c4 100644 --- a/spinifex/daemon/daemon_handlers_vpc.go +++ b/spinifex/daemon/daemon_handlers_vpc.go @@ -188,4 +188,3 @@ func (d *Daemon) ensureDefaultVPCInfrastructure() { } } } - diff --git a/spinifex/gateway/ec2.go b/spinifex/gateway/ec2.go index c779ae49d..263854c19 100644 --- a/spinifex/gateway/ec2.go +++ b/spinifex/gateway/ec2.go @@ -14,6 +14,7 @@ import ( "github.com/mulgadc/spinifex/spinifex/awserrors" gateway_ec2_account "github.com/mulgadc/spinifex/spinifex/gateway/ec2/account" gateway_ec2_eigw "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eigw" + gateway_ec2_eip "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eip" gateway_ec2_igw "github.com/mulgadc/spinifex/spinifex/gateway/ec2/igw" gateway_ec2_image "github.com/mulgadc/spinifex/spinifex/gateway/ec2/image" gateway_ec2_instance "github.com/mulgadc/spinifex/spinifex/gateway/ec2/instance" @@ -22,7 +23,6 @@ import ( gateway_ec2_snapshot "github.com/mulgadc/spinifex/spinifex/gateway/ec2/snapshot" gateway_ec2_tags "github.com/mulgadc/spinifex/spinifex/gateway/ec2/tags" gateway_ec2_volume "github.com/mulgadc/spinifex/spinifex/gateway/ec2/volume" - gateway_ec2_eip "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eip" gateway_ec2_vpc "github.com/mulgadc/spinifex/spinifex/gateway/ec2/vpc" gateway_ec2_zone "github.com/mulgadc/spinifex/spinifex/gateway/ec2/zone" "github.com/mulgadc/spinifex/spinifex/utils" diff --git a/spinifex/handlers/ec2/igw/service_impl.go b/spinifex/handlers/ec2/igw/service_impl.go index 29d6e1417..a18b8ff4c 100644 --- a/spinifex/handlers/ec2/igw/service_impl.go +++ b/spinifex/handlers/ec2/igw/service_impl.go @@ -87,7 +87,6 @@ func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayI } func (s *IGWServiceImpl) createIGW(input *ec2.CreateInternetGatewayInput, accountID, igwID string) (*ec2.CreateInternetGatewayOutput, error) { - record := IGWRecord{ InternetGatewayId: igwID, State: "available", diff --git a/spinifex/handlers/ec2/vpc/external_ipam.go b/spinifex/handlers/ec2/vpc/external_ipam.go index e2bfaa7b7..948b4e580 100644 --- a/spinifex/handlers/ec2/vpc/external_ipam.go +++ b/spinifex/handlers/ec2/vpc/external_ipam.go @@ -21,7 +21,7 @@ const ( type ExternalIPAllocation struct { Type string `json:"type"` // "gateway", "auto_assign", "elastic_ip" AllocationID string `json:"allocation_id,omitempty"` // For elastic IPs - Association string `json:"association,omitempty"` // ENI ID for elastic IPs + Association string `json:"association,omitempty"` // ENI ID for elastic IPs ENIId string `json:"eni_id,omitempty"` // ENI owning this IP InstanceId string `json:"instance_id,omitempty"` // Instance owning this IP Note string `json:"note,omitempty"` // Human-readable note diff --git a/spinifex/handlers/ec2/vpc/security_group.go b/spinifex/handlers/ec2/vpc/security_group.go index a1d1018ef..119bffa70 100644 --- a/spinifex/handlers/ec2/vpc/security_group.go +++ b/spinifex/handlers/ec2/vpc/security_group.go @@ -450,12 +450,12 @@ func (s *VPCServiceImpl) RevokeSecurityGroupEgress(input *ec2.RevokeSecurityGrou // sgRecordToEC2 converts a SecurityGroupRecord to an EC2 SecurityGroup. func (s *VPCServiceImpl) sgRecordToEC2(record *SecurityGroupRecord, accountID string) *ec2.SecurityGroup { sg := &ec2.SecurityGroup{ - GroupId: aws.String(record.GroupId), - GroupName: aws.String(record.GroupName), - Description: aws.String(record.Description), - VpcId: aws.String(record.VpcId), - OwnerId: aws.String(accountID), - IpPermissions: sgRulesToIpPermissions(record.IngressRules), + GroupId: aws.String(record.GroupId), + GroupName: aws.String(record.GroupName), + Description: aws.String(record.Description), + VpcId: aws.String(record.VpcId), + OwnerId: aws.String(accountID), + IpPermissions: sgRulesToIpPermissions(record.IngressRules), IpPermissionsEgress: sgRulesToIpPermissions(record.EgressRules), } diff --git a/spinifex/handlers/iam/service_impl.go b/spinifex/handlers/iam/service_impl.go index dffbc8ae1..83eb615f1 100644 --- a/spinifex/handlers/iam/service_impl.go +++ b/spinifex/handlers/iam/service_impl.go @@ -731,11 +731,13 @@ func (s *IAMServiceImpl) seedAdminAccount(admin *AdminBootstrapData) error { // this account. Without this, the admin account has no default VPC/subnet // and the user must create one manually. if s.natsConn != nil { - evt, _ := json.Marshal(struct { + evt, err := json.Marshal(struct { AccountID string `json:"account_id"` AccountName string `json:"account_name"` }{AccountID: admin.AccountID, AccountName: admin.AccountName}) - if err := s.natsConn.Publish("iam.account.created", evt); err != nil { + if err != nil { + slog.Warn("Failed to marshal account creation event", "accountID", admin.AccountID, "error", err) + } else if err := s.natsConn.Publish("iam.account.created", evt); err != nil { slog.Warn("Failed to publish account creation event for admin account", "accountID", admin.AccountID, "error", err) } } diff --git a/spinifex/services/vpcd/nbdb/model.go b/spinifex/services/vpcd/nbdb/model.go index 08551e848..a98eade4d 100644 --- a/spinifex/services/vpcd/nbdb/model.go +++ b/spinifex/services/vpcd/nbdb/model.go @@ -96,8 +96,8 @@ type LogicalRouterStaticRoute struct { type PortGroup struct { UUID string `ovsdb:"_uuid"` Name string `ovsdb:"name"` - Ports []string `ovsdb:"ports"` // UUIDs of logical switch ports - ACLs []string `ovsdb:"acls"` // UUIDs of ACLs + Ports []string `ovsdb:"ports"` // UUIDs of logical switch ports + ACLs []string `ovsdb:"acls"` // UUIDs of ACLs ExternalIDs map[string]string `ovsdb:"external_ids"` } diff --git a/spinifex/services/vpcd/reconcile_test.go b/spinifex/services/vpcd/reconcile_test.go index 2dd9fa71d..5d52af5d8 100644 --- a/spinifex/services/vpcd/reconcile_test.go +++ b/spinifex/services/vpcd/reconcile_test.go @@ -39,7 +39,7 @@ func TestReconcile_CreatesBootstrapTopology(t *testing.T) { _ = ovn.Connect(context.Background()) topo := NewTopologyHandler(ovn, WithExternalNetwork("pool", []ExternalPoolConfig{{ - Name: "wan", + Name: "wan", RangeStart: "192.168.1.200", RangeEnd: "192.168.1.250", Gateway: "192.168.1.1", diff --git a/spinifex/services/vpcd/topology.go b/spinifex/services/vpcd/topology.go index 4b6edcb48..c075f48c4 100644 --- a/spinifex/services/vpcd/topology.go +++ b/spinifex/services/vpcd/topology.go @@ -655,7 +655,6 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) { gatewayIP := "169.254.0.1" gatewayNetwork := "169.254.0.1/30" wanGateway := "169.254.0.2" - prefixLen := 30 if pool != nil { gip := pool.GatewayIP @@ -663,7 +662,7 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) { gip = pool.RangeStart // Default: first IP in range } gatewayIP = gip - prefixLen = pool.PrefixLen + prefixLen := pool.PrefixLen if prefixLen == 0 { prefixLen = 24 } @@ -759,10 +758,9 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) { // 7. Schedule gateway chassis for HA — tells OVN which hosts can handle external traffic if len(h.chassisNames) > 0 { for i, chassis := range h.chassisNames { - priority := 20 - (i * 5) // First chassis gets highest priority - if priority < 1 { - priority = 1 - } + priority := max( + // First chassis gets highest priority + 20-(i*5), 1) if err := h.ovn.SetGatewayChassis(ctx, gwPortName, chassis, priority); err != nil { slog.Warn("vpcd: failed to set gateway chassis", "port", gwPortName, "chassis", chassis, "priority", priority, "err", err) } else { @@ -1060,7 +1058,6 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string) gatewayIP := "169.254.0.1" gatewayNetwork := "169.254.0.1/30" wanGateway := "169.254.0.2" - prefixLen := 30 if pool != nil { gip := pool.GatewayIP @@ -1068,7 +1065,7 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string) gip = pool.RangeStart } gatewayIP = gip - prefixLen = pool.PrefixLen + prefixLen := pool.PrefixLen if prefixLen == 0 { prefixLen = 24 } @@ -1106,10 +1103,10 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string) reconcileLocalnetOpts["nat-addresses"] = "router" } localnetPort := &nbdb.LogicalSwitchPort{ - Name: extPortName, - Type: "localnet", - Addresses: []string{"unknown"}, - Options: reconcileLocalnetOpts, + Name: extPortName, + Type: "localnet", + Addresses: []string{"unknown"}, + Options: reconcileLocalnetOpts, ExternalIDs: portExtIDs, } if err := h.ovn.CreateLogicalSwitchPort(ctx, extSwitchName, localnetPort); err != nil { @@ -1183,10 +1180,7 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string) // 7. Schedule gateway chassis for i, chassis := range h.chassisNames { - priority := 20 - (i * 5) - if priority < 1 { - priority = 1 - } + priority := max(20-(i*5), 1) if err := h.ovn.SetGatewayChassis(ctx, gwPortName, chassis, priority); err != nil { slog.Warn("vpcd reconcile: failed to set gateway chassis", "chassis", chassis, "err", err) } diff --git a/spinifex/services/vpcd/vpcd.go b/spinifex/services/vpcd/vpcd.go index 18b69817d..2267246d6 100644 --- a/spinifex/services/vpcd/vpcd.go +++ b/spinifex/services/vpcd/vpcd.go @@ -64,8 +64,10 @@ type Config struct { // ExternalInterface is the WAN NIC name (e.g., "enp0s3"). Used to align // the macvlan MAC with the OVN gateway MAC for inbound traffic. ExternalInterface string - // WanBridge is the OVS bridge name for WAN traffic (default "br-wan"). + // WanBridge is the OVS bridge name for WAN traffic. // Maps to OVN logical network "external" via ovn-bridge-mappings. + // Typically "br-ext" (veth mode linking Linux bridge to OVS) or the + // bridge name itself when the default route is already on an OVS bridge. WanBridge string // BridgeMode is "direct" or "macvlan". Direct bridge adds the WAN NIC // directly to the WAN bridge; macvlan creates a sub-interface. When empty,