bringing local networking changes onto dev branch

This commit is contained in:
tomnewton-mulga
2026-05-01 12:13:34 +10:00
parent 09638a874d
commit 9d665f9dde
10 changed files with 213 additions and 209 deletions
+4 -3
View File
@@ -123,9 +123,10 @@ printf "nameserver 1.1.1.1\nnameserver 8.8.8.8\n" > /etc/resolvconf/resolv.conf.
resolvconf -u
# Configure OVN networking.
# br-wan (and br-lan if present) are Linux bridges created by the installer.
# setup-ovn.sh auto-detects br-wan as the default route device (Linux bridge)
# and wires it to OVS via a veth pair — non-destructive, SSH-safe.
# br-wan (and br-lan if present) are Linux bridges managed by systemd-networkd
# (declared in /etc/systemd/network/ by the installer). setup-ovn.sh
# auto-detects br-wan as the default route device and wires it to OVS via a
# veth pair — non-destructive, SSH-safe.
%s
# Write the banner (first time, spinifex-banner service will do this on reboot)
+167 -74
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"io"
"log/slog"
"net"
"os"
"os/exec"
"path/filepath"
@@ -299,97 +300,71 @@ func installSpinifex(cfg *Config) error {
return err
}
// dhcpcd.conf: only apply a short DHCP timeout on the optional LAN bridge.
// br-wan keeps the dhcpcd default (30 s) so it reliably gets a lease even
// on physical switches that take time to come up. br-lan is brought up by
// spinifex-lan-bridge.service (non-critical), so failing fast there is fine.
// Writing this here (after copyRootfs) ensures the installed system always
// has the correct config regardless of what the rootfs image contains.
dhcpcdConf := "# Generated by Spinifex installer\ninterface br-lan\ntimeout 10\n"
if err := os.WriteFile(filepath.Join(mountRoot, "etc/dhcpcd.conf"), []byte(dhcpcdConf), 0o644); err != nil {
return err
}
// Mask dhcpcd.service so only ifupdown controls DHCP on the bridges.
// Trixie's dhcpcd-base ships a standalone dhcpcd.service that races with
// ifupdown's own dhcpcd invocations, causing duplicate leases or IP flapping.
// dhcpcd-base is present on the installed system (used by setup-ovn.sh for
// macvlan mode). Mask the standalone dhcpcd.service so it never auto-starts
// and races with systemd-networkd's built-in DHCP client on br-wan.
if err := maskSystemdUnit(mountRoot, "dhcpcd.service"); err != nil {
slog.Warn("installSpinifex: failed to mask dhcpcd.service", "err", err)
}
return systemd.WriteNetworkingDropIn(mountRoot)
return nil
}
func writeNetworkConfig(cfg *Config) error {
// IPs live on Linux bridges (br-wan, br-lan), not on the physical NICs.
// This lets OVN/OVS wire into the bridges non-destructively via veth pairs
// without ever moving the IP off a live interface (SSH-safe).
//
// Only br-wan uses `auto` so networking.service brings it up at boot.
// br-lan deliberately omits the `auto` stanza — it is brought up by
// spinifex-lan-bridge.service *after* network-online.target, so a missing
// LAN cable or slow switch can never block networking.service or firstboot.
var b strings.Builder
b.WriteString("# Generated by Spinifex installer\nsource /etc/network/interfaces.d/*\n\nauto lo\niface lo inet loopback\n\n")
writeBridge := func(nicIface, bridgeName string, dhcp bool, addr, mask, gw string, dns []string, ssid, wifiPass string, hasGateway, autoStart bool) {
comment := strings.ToUpper(bridgeName[3:]) // "br-wan" → "WAN", "br-lan" → "LAN"
fmt.Fprintf(&b, "# %s NIC\nauto %s\niface %s inet manual\n", comment, nicIface, nicIface)
if ssid != "" {
fmt.Fprintf(&b, " wpa-ssid %s\n wpa-psk %s\n", ssid, wifiPass)
}
if !dhcp {
for _, ns := range dns {
ns = strings.TrimSpace(ns)
if ns != "" {
fmt.Fprintf(&b, " dns-nameservers %s\n", ns)
}
}
}
b.WriteString("\n")
// Bridge stanza.
if autoStart {
fmt.Fprintf(&b, "# %s bridge\nauto %s\n", comment, bridgeName)
} else {
fmt.Fprintf(&b, "# %s bridge — brought up by spinifex-lan-bridge.service\n", comment)
}
if dhcp {
fmt.Fprintf(&b, "iface %s inet dhcp\n", bridgeName)
} else {
fmt.Fprintf(&b, "iface %s inet static\n address %s\n netmask %s\n", bridgeName, addr, mask)
if hasGateway && gw != "" {
fmt.Fprintf(&b, " gateway %s\n", gw)
}
}
fmt.Fprintf(&b, " bridge_ports %s\n bridge_stp off\n bridge_fd 0\n bridge_maxwait 0\n\n", nicIface)
}
writeBridge(cfg.WANInterface, "br-wan", cfg.WANDHCPMode,
cfg.WANAddress, cfg.WANMask, cfg.WANGateway, cfg.WANDNS,
cfg.WANWiFiSSID, cfg.WANWiFiPass, true, true)
if cfg.LANInterface != "" {
writeBridge(cfg.LANInterface, "br-lan", cfg.LANDHCPMode,
cfg.LANAddress, cfg.LANMask, "", cfg.LANDNS,
cfg.LANWiFiSSID, cfg.LANWiFiPass, false, false)
}
if err := os.WriteFile(filepath.Join(mountRoot, "etc/network/interfaces"), []byte(b.String()), 0o644); err != nil {
// systemd-networkd owns the full lifecycle: it creates the bridge NetDevs,
// enslaves the physical NICs, and runs the DHCP client. This means the
// veth pair that setup-ovn.sh adds later (veth-wan-br) is a port of a
// networkd-known bridge, so a networkctl reload or reboot never orphans it.
netdDir := filepath.Join(mountRoot, "etc/systemd/network")
if err := os.MkdirAll(netdDir, 0o755); err != nil {
return err
}
if err := writeNetworkdBridge(netdDir, cfg.WANInterface, "br-wan", false,
cfg.WANDHCPMode, cfg.WANAddress, cfg.WANMask, cfg.WANGateway, cfg.WANDNS,
cfg.WANWiFiSSID, cfg.WANWiFiPass); err != nil {
return err
}
// Write a non-critical systemd unit for br-lan so it comes up after
// network-online.target without blocking the boot path.
if cfg.LANInterface != "" {
if err := writeNetworkdBridge(netdDir, cfg.LANInterface, "br-lan", true,
cfg.LANDHCPMode, cfg.LANAddress, cfg.LANMask, "", cfg.LANDNS,
cfg.LANWiFiSSID, cfg.LANWiFiPass); err != nil {
return err
}
// br-lan timing is controlled by spinifex-lan-bridge.service, not
// networkd auto-activation — ActivationPolicy=manual in the .network
// file ensures networkd creates the bridge but does not bring it up.
if err := systemd.WriteLANBridgeUnit(mountRoot); err != nil {
return fmt.Errorf("lan bridge unit: %w", err)
}
}
// Disable IPv6 on the bridge interfaces. We only use IPv4 for management
// and OVN tunnels; without this, dhcpcd logs "no IPv6 routers available"
// errors and the boot journal is noisy.
// Unmask systemd-networkd-wait-online on the installed system and scope
// it to br-wan only. The live ISO masks this service (build-rootfs.sh) to
// avoid blocking the installer environment (which has no br-wan). The mask
// is copied to the installed system by copyRootfs and must be removed here
// so that network-online.target — and therefore spinifex-firstboot.service
// (After=network-online.target) — does not fire before br-wan has its
// DHCP lease and default route. Without this, setup-ovn.sh finds no default
// route and exits early, creating no veth pair.
//
// The --interface=br-wan scope means br-lan (ActivationPolicy=manual) never
// blocks the wait; --timeout=60 caps the delay on a cold switch.
waitOnlineMask := filepath.Join(mountRoot, "etc/systemd/system/systemd-networkd-wait-online.service")
_ = os.Remove(waitOnlineMask)
waitOnlineDir := filepath.Join(mountRoot, "etc/systemd/system/systemd-networkd-wait-online.service.d")
if err := os.MkdirAll(waitOnlineDir, 0o755); err != nil {
return err
}
waitOnlineConf := "[Service]\nExecStart=\nExecStart=/lib/systemd/systemd-networkd-wait-online --interface=br-wan --timeout=60\n"
if err := os.WriteFile(filepath.Join(waitOnlineDir, "spinifex-wan-only.conf"), []byte(waitOnlineConf), 0o644); err != nil {
return err
}
// Disable IPv6 via sysctl — belt-and-suspenders alongside IPv6AcceptRA=no
// in the networkd .network files.
bridges := []string{"br-wan"}
if cfg.LANInterface != "" {
bridges = append(bridges, "br-lan")
@@ -432,6 +407,124 @@ func writeNetworkConfig(cfg *Config) error {
return nil
}
// writeNetworkdBridge writes the three systemd-networkd files that configure a
// physical NIC enslaved to a Linux bridge:
//
// - {prio}-spinifex-{suffix}-nic.network enslaves the NIC to the bridge
// - {prio+1}-spinifex-{suffix}.netdev declares the bridge device
// - {prio+1}-spinifex-{suffix}.network configures IP on the bridge
//
// manual=true sets ActivationPolicy=manual so networkd creates the bridge but
// does not auto-activate it — used for br-lan, which spinifex-lan-bridge.service
// activates after network-online.target.
func writeNetworkdBridge(dir, nicIface, bridgeName string, manual, dhcp bool,
addr, mask, gw string, dns []string, wifiSSID, wifiPass string) error {
suffix := strings.TrimPrefix(bridgeName, "br-")
nicPrio, brPrio := 10, 11
if manual {
nicPrio, brPrio = 12, 13
}
// NIC .network — enslaves the physical NIC to the bridge.
nicNet := fmt.Sprintf("[Match]\nName=%s\n\n[Network]\nBridge=%s\n", nicIface, bridgeName)
nicFile := fmt.Sprintf("%02d-spinifex-%s-nic.network", nicPrio, suffix)
if err := os.WriteFile(filepath.Join(dir, nicFile), []byte(nicNet), 0o644); err != nil {
return err
}
// Bridge .netdev — declares the bridge device.
brNetdev := fmt.Sprintf("[NetDev]\nName=%s\nKind=bridge\n\n[Bridge]\nSTP=no\n", bridgeName)
brNetdevFile := fmt.Sprintf("%02d-spinifex-%s.netdev", brPrio, suffix)
if err := os.WriteFile(filepath.Join(dir, brNetdevFile), []byte(brNetdev), 0o644); err != nil {
return err
}
// Bridge .network — IP configuration.
var b strings.Builder
fmt.Fprintf(&b, "[Match]\nName=%s\n\n", bridgeName)
if manual {
b.WriteString("[Link]\nActivationPolicy=manual\nRequiredForOnline=no\n\n")
}
b.WriteString("[Network]\n")
if dhcp {
b.WriteString("DHCP=ipv4\n")
} else {
cidr, err := addrCIDR(addr, mask)
if err != nil {
return fmt.Errorf("bridge %s: %w", bridgeName, err)
}
fmt.Fprintf(&b, "Address=%s\n", cidr)
if gw != "" {
fmt.Fprintf(&b, "Gateway=%s\n", gw)
}
for _, ns := range dns {
if ns = strings.TrimSpace(ns); ns != "" {
fmt.Fprintf(&b, "DNS=%s\n", ns)
}
}
}
b.WriteString("IPv6AcceptRA=no\nConfigureWithoutCarrier=yes\n")
if manual && dhcp {
// br-lan is non-critical; fail fast on DHCP so a missing LAN cable
// does not stall spinifex-lan-bridge.service indefinitely.
b.WriteString("\n[DHCP]\nTimeout=10\n")
}
brNetFile := fmt.Sprintf("%02d-spinifex-%s.network", brPrio, suffix)
if err := os.WriteFile(filepath.Join(dir, brNetFile), []byte(b.String()), 0o644); err != nil {
return err
}
if wifiSSID != "" {
if err := writeWPASupplicant(nicIface, wifiSSID, wifiPass); err != nil {
return err
}
}
return nil
}
// addrCIDR converts a dotted-decimal address + subnet mask to CIDR notation.
func addrCIDR(addr, dotMask string) (string, error) {
if net.ParseIP(addr) == nil {
return "", fmt.Errorf("invalid IP: %s", addr)
}
maskIP := net.ParseIP(dotMask)
if maskIP == nil {
return "", fmt.Errorf("invalid mask: %s", dotMask)
}
m := net.IPMask(maskIP.To4())
ones, bits := m.Size()
if bits == 0 {
return "", fmt.Errorf("non-contiguous or zero subnet mask: %s", dotMask)
}
return fmt.Sprintf("%s/%d", addr, ones), nil
}
// writeWPASupplicant writes a wpa_supplicant config for nicIface and enables
// the per-interface wpa_supplicant@{iface}.service so authentication completes
// before networkd enslaves the NIC to the bridge.
func writeWPASupplicant(nicIface, ssid, psk string) error {
conf := fmt.Sprintf(
"ctrl_interface=DIR=/run/wpa_supplicant GROUP=netdev\nupdate_config=1\n\nnetwork={\n\tssid=%q\n\tpsk=%q\n}\n",
ssid, psk,
)
wpaDir := filepath.Join(mountRoot, "etc/wpa_supplicant")
if err := os.MkdirAll(wpaDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(wpaDir, "wpa_supplicant-"+nicIface+".conf"), []byte(conf), 0o600); err != nil {
return err
}
// Enable via symlink into multi-user.target.wants pointing at the
// package-provided template unit.
wantsDir := filepath.Join(mountRoot, "etc/systemd/system/multi-user.target.wants")
if err := os.MkdirAll(wantsDir, 0o755); err != nil {
return err
}
link := filepath.Join(wantsDir, "wpa_supplicant@"+nicIface+".service")
_ = os.Remove(link)
return os.Symlink("/lib/systemd/system/wpa_supplicant@.service", link)
}
func installBootloader(disk string) error {
// grub-install runs in the live environment (not chroot) using the
// grub-pc-bin and grub-efi-amd64-bin packages already present on the ISO.
+8 -18
View File
@@ -25,10 +25,13 @@ import (
"path/filepath"
)
// WriteLANBridgeUnit installs a non-critical oneshot service that brings up
// br-lan *after* network-online.target. This keeps br-lan out of
// networking.service entirely — a missing LAN cable or DHCP timeout on the
// secondary bridge can never stall the management interface or firstboot.
// WriteLANBridgeUnit installs a non-critical oneshot service that activates
// br-lan via networkctl *after* network-online.target. br-lan has
// ActivationPolicy=manual in its networkd .network file, so networkd creates
// the bridge at boot but does not bring it up automatically. This service
// triggers the activation once the WAN path is confirmed online, ensuring a
// missing LAN cable or DHCP timeout on the secondary bridge never stalls
// the management interface or firstboot.
func WriteLANBridgeUnit(root string) error {
unit := `[Unit]
Description=Spinifex LAN bridge (non-critical)
@@ -37,7 +40,7 @@ Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/sbin/ifup br-lan
ExecStart=/usr/bin/networkctl up br-lan
RemainAfterExit=yes
# Failure is non-critical — cable unplugged or switch not ready at boot.
SuccessExitStatus=0 1
@@ -60,19 +63,6 @@ WantedBy=multi-user.target
return os.Symlink("/etc/systemd/system/spinifex-lan-bridge.service", link)
}
// WriteNetworkingDropIn writes a networking.service drop-in that treats exit
// code 1 as success. This prevents a secondary interface failure (e.g. br-lan
// DHCP timeout when no cable is plugged in) from blocking network-online.target
// and therefore spinifex-firstboot.service.
func WriteNetworkingDropIn(root string) error {
dir := filepath.Join(root, "etc/systemd/system/networking.service.d")
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
dropIn := "[Service]\nSuccessExitStatus=0 1\n"
return os.WriteFile(filepath.Join(dir, "spinifex-optional-ifaces.conf"), []byte(dropIn), 0o644)
}
// WriteFirstbootUnit writes the spinifex-firstboot.service oneshot unit that
// runs the firstboot provisioning script on the first real boot after
// installation. The script writes /var/lib/spinifex/.firstboot-done on
+12 -12
View File
@@ -388,20 +388,20 @@ if [ -n "$WAN_BRIDGE" ]; then
# starts with the OVS port pointing at a nonexistent peer and
# silently falls back to direct mode (Fix 1, mulga-998.b).
#
# Declare the Linux bridge as a NetDev too (even though it's
# already created by ifupdown) so networkd recognises it and the
# `Bridge=$LINUX_BRIDGE` directive below resolves. Without this,
# networkd logs `br-wan NetDev could not be found, ignoring
# assignment` and veth-wan-br ends up orphaned with no master,
# which crashes vpcd's veth bridge mode sanity check on reboot.
# networkd's Bridge= directive requires the target bridge to be a
# known NetDev. On ISO-installed nodes the installer writes
# 11-spinifex-br-wan.netdev, which the gate below detects and
# skips this write. On binary-installed nodes where the operator
# manages br-wan outside networkd (e.g. cloud-init), this file
# fills the gap so veth-wan-br resolves its bridge on reboot.
# `Failed to create netdev: File exists` is harmless — networkd
# then matches the existing kernel bridge by name+kind.
# matches the existing kernel bridge by name+kind.
#
# Gate: skip the write if any operator-managed .netdev (cloud-init,
# netplan, manual op) already declares this bridge — don't clobber
# custom networkd config. networkd searches /etc, /run, /usr/lib in
# that priority order; check all three. Our own file is excluded so
# idempotent re-runs still rewrite when needed.
# Gate: skip the write if any .netdev (installer, cloud-init,
# netplan, manual) already declares this bridge — don't clobber
# existing networkd config. networkd searches /etc, /run, /usr/lib;
# check all three. Our own file is excluded so idempotent re-runs
# still rewrite when needed.
BR_WAN_NETDEV="/etc/systemd/network/14-spinifex-br-wan.netdev"
VETH_NETDEV="/etc/systemd/network/15-spinifex-veth-wan.netdev"
VETH_NETWORK="/etc/systemd/network/15-spinifex-veth-wan.network"
+5
View File
@@ -690,6 +690,10 @@ func (d *Daemon) Start() error {
if node, ok := d.clusterConfig.Nodes[d.clusterConfig.Node]; ok {
dhcpBindBridge = node.VPCD.DhcpBindBridge
}
gwMAC := ""
if d.clusterConfig.Bootstrap.VpcId != "" {
gwMAC = utils.HashMAC("gw-" + d.clusterConfig.Bootstrap.VpcId)
}
for _, p := range d.clusterConfig.Network.ExternalPools {
pools = append(pools, handlers_ec2_vpc.ExternalPoolConfig{
Name: p.Name,
@@ -702,6 +706,7 @@ func (d *Daemon) Start() error {
Region: p.Region,
AZ: p.AZ,
DhcpBindBridge: dhcpBindBridge,
GatewayMAC: gwMAC,
GwLrpRangeStart: p.GwLrpRangeStart,
GwLrpRangeEnd: p.GwLrpRangeEnd,
})
+9 -1
View File
@@ -68,6 +68,13 @@ type ExternalPoolConfig struct {
Region string
AZ string
DhcpBindBridge string // Bridge where the DHCP AF_PACKET socket binds (e.g. "br-wan"). Linux bridge in veth mode; OVS bridge in direct mode. Never "br-ext".
// GatewayMAC, when set, is used as the chaddr in per-VM DHCP DORAs so
// that the upstream DHCP binding matches the MAC that OVN GARPs use (the
// gateway LRP MAC). Without this, the DHCP server binds the public IP to
// the generated ENI-derived MAC while OVN announces a different MAC via
// GARP, causing home routers that use their DHCP lease table as ARP to
// forward traffic to a non-existent interface.
GatewayMAC string
// GwLrpRangeStart/End reserves a sub-range of the LAN for OVN gateway
// LRP IPs in centralized NAT mode (mulga-siv-36). IPAM must skip these
// addresses or the per-VM EIP allocator and vpcd will fight over them.
@@ -181,6 +188,7 @@ func (m *ExternalIPAM) initPool(pool ExternalPoolConfig) error {
"gateway-"+pool.Name,
"mulga-spinifex-gw",
pool.Name,
"",
)
if dhcpErr != nil {
return fmt.Errorf("DHCP gateway lease for pool %q: %w", pool.Name, dhcpErr)
@@ -271,7 +279,7 @@ func (m *ExternalIPAM) allocateFromPool(poolName, allocType, allocID, eniID, ins
var dhcpLease *dhcp.LeaseResult
if pool != nil && pool.IsDHCP() {
hostname, vendorClass := dhcpIdentityOptions(eniID, instanceID, poolName)
lease, err := dhcp.RequestAcquire(m.nc, pool.DhcpBindBridge, clientID, hostname, vendorClass, poolName)
lease, err := dhcp.RequestAcquire(m.nc, pool.DhcpBindBridge, clientID, hostname, vendorClass, poolName, pool.GatewayMAC)
if err != nil {
return "", fmt.Errorf("DHCP lease for %s: %w", clientID, err)
}
@@ -591,100 +591,6 @@ func TestExternalIPAM_DHCPGatewayFromLease(t *testing.T) {
assert.Equal(t, "mulga-spinifex-gw", got.VendorClass)
}
func TestObtainDHCPLease_RequestTimeout(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
// Stub never responds — verifies that a hung vpcd surfaces as a
// wrapped NATS timeout error rather than hanging the caller.
sub, err := nc.Subscribe(dhcp.TopicAcquire, func(*nats.Msg) {})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
prev := dhcpNATSTimeout
dhcpNATSTimeout = 150 * time.Millisecond
t.Cleanup(func() { dhcpNATSTimeout = prev })
prevMax := dhcpAcquireMaxAttempts
prevDelay := dhcpAcquireRetryDelay
dhcpAcquireMaxAttempts = 2
dhcpAcquireRetryDelay = 10 * time.Millisecond
t.Cleanup(func() {
dhcpAcquireMaxAttempts = prevMax
dhcpAcquireRetryDelay = prevDelay
})
_, err = ObtainDHCPLease(nc, "br-wan", "eni-timeout", "eni-timeout", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "dhcp acquire NATS request")
}
func TestReleaseDHCPLease_RequestTimeout(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
sub, err := nc.Subscribe(dhcp.TopicRelease, func(*nats.Msg) {})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
prev := dhcpNATSTimeout
dhcpNATSTimeout = 150 * time.Millisecond
t.Cleanup(func() { dhcpNATSTimeout = prev })
err = ReleaseDHCPLease(nc, "eni-timeout")
assert.ErrorContains(t, err, "dhcp release NATS request")
}
func TestObtainDHCPLease_MalformedReply(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
sub, err := nc.Subscribe(dhcp.TopicAcquire, func(msg *nats.Msg) {
_ = msg.Respond([]byte("not json"))
})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
_, err = ObtainDHCPLease(nc, "br-wan", "eni-malformed", "eni-malformed", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "unmarshal dhcp acquire reply")
}
func TestReleaseDHCPLease_MalformedReply(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
sub, err := nc.Subscribe(dhcp.TopicRelease, func(msg *nats.Msg) {
_ = msg.Respond([]byte("not json"))
})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
err = ReleaseDHCPLease(nc, "eni-malformed")
assert.ErrorContains(t, err, "unmarshal dhcp release reply")
}
func TestReleaseDHCPLease_ReplyError(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
sub, err := nc.Subscribe(dhcp.TopicRelease, func(msg *nats.Msg) {
data, _ := json.Marshal(dhcp.ReleaseReplyMsg{Error: "vpcd rejected release"})
_ = msg.Respond(data)
})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
err = ReleaseDHCPLease(nc, "eni-rejected")
assert.ErrorContains(t, err, "vpcd rejected release")
}
func TestObtainDHCPLease_Guards(t *testing.T) {
_, err := ObtainDHCPLease(nil, "br-wan", "eni-1", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "NATS connection is required")
_, nc := testutil.StartTestNATS(t)
_, err = ObtainDHCPLease(nc, "", "eni-1", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "bridge name is required")
_, err = ObtainDHCPLease(nc, "br-wan", "", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "client ID is required")
}
func TestReleaseDHCPLease_NoopWhenNilOrEmpty(t *testing.T) {
assert.NoError(t, ReleaseDHCPLease(nil, "eni-1"))
_, nc := testutil.StartTestNATS(t)
assert.NoError(t, ReleaseDHCPLease(nc, ""))
}
func TestExternalIPAM_DHCPGatewayErrorPropagates(t *testing.T) {
_, nc, js := testutil.StartTestJetStream(t)
stub := newDHCPStub(t, nc)
+2 -1
View File
@@ -46,7 +46,7 @@ type LeaseResult struct {
// clientID: a second call with the same clientID while a live lease exists
// returns the same lease without a fresh DORA, so CAS retry loops on the
// caller are safe.
func RequestAcquire(nc *nats.Conn, bridge, clientID, hostname, vendorClass, poolName string) (LeaseResult, error) {
func RequestAcquire(nc *nats.Conn, bridge, clientID, hostname, vendorClass, poolName, hwAddr string) (LeaseResult, error) {
if nc == nil {
return LeaseResult{}, fmt.Errorf("DHCP lease: NATS connection is required")
}
@@ -63,6 +63,7 @@ func RequestAcquire(nc *nats.Conn, bridge, clientID, hostname, vendorClass, pool
Hostname: hostname,
VendorClass: vendorClass,
PoolName: poolName,
HWAddr: hwAddr,
}
data, err := json.Marshal(req)
if err != nil {
@@ -30,7 +30,7 @@ func TestRequestAcquire_RequestTimeout(t *testing.T) {
AcquireRetryDelay = prevDelay
})
_, err = RequestAcquire(nc, "br-wan", "eni-timeout", "eni-timeout", "mulga-spinifex", "wan")
_, err = RequestAcquire(nc, "br-wan", "eni-timeout", "eni-timeout", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "dhcp acquire NATS request")
}
@@ -56,7 +56,7 @@ func TestRequestAcquire_MalformedReply(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
_, err = RequestAcquire(nc, "br-wan", "eni-malformed", "eni-malformed", "mulga-spinifex", "wan")
_, err = RequestAcquire(nc, "br-wan", "eni-malformed", "eni-malformed", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "unmarshal dhcp acquire reply")
}
@@ -86,14 +86,14 @@ func TestRequestRelease_ReplyError(t *testing.T) {
}
func TestRequestAcquire_Guards(t *testing.T) {
_, err := RequestAcquire(nil, "br-wan", "eni-1", "eni-1", "mulga-spinifex", "wan")
_, err := RequestAcquire(nil, "br-wan", "eni-1", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "NATS connection is required")
_, nc := testutil.StartTestNATS(t)
_, err = RequestAcquire(nc, "", "eni-1", "eni-1", "mulga-spinifex", "wan")
_, err = RequestAcquire(nc, "", "eni-1", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "bridge name is required")
_, err = RequestAcquire(nc, "br-wan", "", "eni-1", "mulga-spinifex", "wan")
_, err = RequestAcquire(nc, "br-wan", "", "eni-1", "mulga-spinifex", "wan", "")
assert.ErrorContains(t, err, "client ID is required")
}
+1 -1
View File
@@ -1634,7 +1634,7 @@ func (h *TopologyHandler) allocateGatewayLRPIPViaDHCP(ctx context.Context, vpcId
clientID := gwLrpClientID(vpcId)
hostname := "spinifex-gw-" + vpcId
vendorClass := "mulga-spinifex-gw-lrp"
lease, dhcpErr := dhcp.RequestAcquire(h.nc, pool.DhcpBindBridge, clientID, hostname, vendorClass, pool.Name)
lease, dhcpErr := dhcp.RequestAcquire(h.nc, pool.DhcpBindBridge, clientID, hostname, vendorClass, pool.Name, "")
if dhcpErr != nil {
return "", 0, fmt.Errorf("dhcp acquire gw LRP IP for vpc %s: %w", vpcId, dhcpErr)
}