@@ -54,10 +54,10 @@ def _use_sysfs(self):
5454 """
5555 return os .path .isdir ("/sys/class/net" )
5656
57- def _ifconfig_interfaces (self ):
57+ def _ifconfig (self ):
5858 """Lazily parse ``ifconfig -a`` once, for the non-sysfs (BSD) code path."""
5959 if not hasattr (self , "_ifconfig_cache" ):
60- self ._ifconfig_cache = Ifconfig (). interfaces
60+ self ._ifconfig_cache = Ifconfig ()
6161 return self ._ifconfig_cache
6262
6363 def _interface_names (self ):
@@ -68,7 +68,7 @@ def _interface_names(self):
6868 for i in os .listdir ("/sys/class/net/" )
6969 if os .path .islink ("/sys/class/net/{}" .format (i ))
7070 ]
71- return list (self ._ifconfig_interfaces () .keys ())
71+ return list (self ._ifconfig (). interfaces .keys ())
7272
7373 def _interface_mac (self , interface , ethtool ):
7474 if config .network .primary_mac == "permanent" and ethtool and ethtool .get ("mac_address" ):
@@ -78,7 +78,7 @@ def _interface_mac(self, interface, ethtool):
7878 if mac == "00:00:00:00:00:00" :
7979 mac = None
8080 else :
81- mac = self ._ifconfig_interfaces () .get (interface , {}).get ("mac" )
81+ mac = self ._ifconfig (). interfaces .get (interface , {}).get ("mac" )
8282 if mac == "00:00:00:00:00:00" :
8383 mac = None
8484 if mac :
@@ -88,7 +88,7 @@ def _interface_mac(self, interface, ethtool):
8888 def _interface_mtu (self , interface ):
8989 if self ._use_sysfs ():
9090 return int (open ("/sys/class/net/{}/mtu" .format (interface ), "r" ).read ().strip ())
91- return self ._ifconfig_interfaces () .get (interface , {}).get ("mtu" )
91+ return self ._ifconfig (). interfaces .get (interface , {}).get ("mtu" )
9292
9393 def _interface_bonding (self , interface ):
9494 if self ._use_sysfs () and os .path .isdir ("/sys/class/net/{}/bonding" .format (interface )):
@@ -107,6 +107,76 @@ def _interface_virtual(self, interface):
107107 )
108108 )
109109
110+ def _carp_vip_addresses (self ):
111+ """CARP virtual IPs: addresses carrying a `vhid` in ``ifconfig``.
112+
113+ CARP is *BSD-only and read from ``ifconfig``; the sysfs (Linux) path has
114+ no equivalent, so this returns nothing there.
115+ """
116+ if not config .network .vip_carp or self ._use_sysfs ():
117+ return set ()
118+ return set (self ._ifconfig ().carp_addresses )
119+
120+ def _tunnel_vip_addresses (self ):
121+ """VIPs on IP-tunnel interfaces: a /32 (or /128) on an interface whose
122+ sysfs ARPHRD type is IPIP/IP6IP6/SIT/GRE/IP6GRE (e.g. LVS-TUN)."""
123+ if not config .network .vip_tunnel or not self ._use_sysfs ():
124+ return set ()
125+ tunnel_types = ("768" , "769" , "776" , "778" , "823" )
126+ vips = set ()
127+ for interface in self ._interface_names ():
128+ try :
129+ with open ("/sys/class/net/{}/type" .format (interface )) as fh :
130+ if fh .read ().strip () not in tunnel_types :
131+ continue
132+ except OSError :
133+ continue
134+ for family in (netifaces .AF_INET , netifaces .AF_INET6 ):
135+ for addr in netifaces .ifaddresses (interface ).get (family , []):
136+ bits = IPAddress (addr ["mask" ].split ("/" )[0 ]).netmask_bits ()
137+ if (family == netifaces .AF_INET and bits == 32 ) or (
138+ family == netifaces .AF_INET6 and bits == 128
139+ ):
140+ vips .add (addr ["addr" ].split ("%" )[0 ])
141+ return vips
142+
143+ def _loopback_vip_addresses (self ):
144+ """Non-localhost addresses configured on a loopback interface (lo/lo0)."""
145+ if not config .network .vip_loopback :
146+ return set ()
147+ vips = set ()
148+ for interface in self ._interface_names ():
149+ if not re .match (r"^lo\d*$" , interface ):
150+ continue
151+ for family in (netifaces .AF_INET , netifaces .AF_INET6 ):
152+ for addr in netifaces .ifaddresses (interface ).get (family , []):
153+ a = addr ["addr" ].split ("%" )[0 ]
154+ ipobj = IPAddress (a )
155+ if ipobj .is_loopback () or ipobj .is_link_local ():
156+ continue
157+ vips .add (a )
158+ return vips
159+
160+ def vip_roles (self ):
161+ """Map locally-detected VIP addresses to NetBox IP role labels.
162+
163+ Each detector is opt-in via config (``network.vip_carp`` / ``vip_tunnel``
164+ / ``vip_loopback``), all default off, so with none enabled this returns
165+ ``{}`` and IP handling is unchanged. A detected role lets
166+ :meth:`create_or_update_netbox_ip_on_interface` mark the address so peers
167+ sharing it each keep their own record instead of stealing it.
168+ """
169+ if not hasattr (self , "_vip_roles_cache" ):
170+ roles = {}
171+ for addr in self ._carp_vip_addresses ():
172+ roles [addr ] = "CARP"
173+ for addr in self ._tunnel_vip_addresses ():
174+ roles .setdefault (addr , "VIP" )
175+ for addr in self ._loopback_vip_addresses ():
176+ roles .setdefault (addr , "VIP" )
177+ self ._vip_roles_cache = roles
178+ return self ._vip_roles_cache
179+
110180 def scan (self ):
111181 nics = []
112182 for interface in self ._interface_names ():
@@ -421,9 +491,8 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface):
421491 * If IP exists and isn't assigned, take it
422492 * If IP exists and interface is wrong, change interface
423493 """
424- netbox_ips = nb .ipam .ip_addresses .filter (
425- address = ip ,
426- )
494+ role = self .vip_roles ().get (ip .split ("/" )[0 ])
495+ netbox_ips = list (nb .ipam .ip_addresses .filter (address = ip ))
427496 if not netbox_ips :
428497 logging .info ("Create new IP {ip} on {interface}" .format (ip = ip , interface = interface ))
429498 query_params = {
@@ -432,31 +501,43 @@ def create_or_update_netbox_ip_on_interface(self, ip, interface):
432501 "assigned_object_type" : self .assigned_object_type ,
433502 "assigned_object_id" : interface .id ,
434503 }
504+ if role :
505+ query_params ["role" ] = self .ipam_choices ["ip-address:role" ][role ]
435506
436507 netbox_ip = nb .ipam .ip_addresses .create (** query_params )
437508 return netbox_ip
438509
439- netbox_ip = list (netbox_ips )[0 ]
440- # If IP exists in anycast
441- if netbox_ip .role and netbox_ip .role .label == "Anycast" :
442- logging .debug ("IP {} is Anycast.." .format (ip ))
443- unassigned_anycast_ip = [x for x in netbox_ips if x .interface is None ]
444- assigned_anycast_ip = [
445- x for x in netbox_ips if x .interface and x .interface .id == interface .id
446- ]
447- # use the first available anycast ip
448- if len (unassigned_anycast_ip ):
449- logging .info ("Assigning existing Anycast IP {} to interface" .format (ip ))
450- netbox_ip = unassigned_anycast_ip [0 ]
451- netbox_ip .interface = interface
510+ netbox_ip = netbox_ips [0 ]
511+ existing_role = netbox_ip .role .label if netbox_ip .role else None
512+ # Multi-assignable / shared IPs (Anycast, plus any detected VIP role):
513+ # each host keeps its own record for the shared address instead of
514+ # stealing it. With VIP detection off (role is None) this triggers only
515+ # for a pre-existing Anycast role -- as before -- but now via
516+ # assigned_object_id rather than the removed `.interface` attribute.
517+ if role or existing_role == "Anycast" :
518+ role_label = role or existing_role
519+ logging .debug ("IP {} is {} (multi-assignable).." .format (ip , role_label ))
520+ assigned_here = [x for x in netbox_ips if x .assigned_object_id == interface .id ]
521+ unassigned = [x for x in netbox_ips if x .assigned_object_id is None ]
522+ if assigned_here :
523+ netbox_ip = assigned_here [0 ]
524+ elif unassigned :
525+ logging .info ("Assigning existing {} IP {} to interface" .format (role_label , ip ))
526+ netbox_ip = unassigned [0 ]
527+ netbox_ip .assigned_object_type = self .assigned_object_type
528+ netbox_ip .assigned_object_id = interface .id
529+ if role :
530+ netbox_ip .role = self .ipam_choices ["ip-address:role" ][role ]
452531 netbox_ip .save ()
453- # or if everything is assigned to other servers
454- elif not len (assigned_anycast_ip ):
455- logging .info ("Creating Anycast IP {} and assigning it to interface" .format (ip ))
532+ else :
533+ # every existing copy is assigned to another host; create our own
534+ logging .info (
535+ "Creating {} IP {} and assigning it to interface" .format (role_label , ip )
536+ )
456537 query_params = {
457538 "address" : ip ,
458539 "status" : "active" ,
459- "role" : self .ipam_choices ["ip-address:role" ]["Anycast" ],
540+ "role" : self .ipam_choices ["ip-address:role" ][role_label ],
460541 "tenant" : self .tenant .id if self .tenant else None ,
461542 "assigned_object_type" : self .assigned_object_type ,
462543 "assigned_object_id" : interface .id ,
0 commit comments