mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-22 12:16:52 +00:00
bringing local networking changes onto dev branch
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user