osism 0.20250804.0__py3-none-any.whl → 0.20250824.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osism/api.py +154 -2
- osism/commands/baremetal.py +168 -0
- osism/commands/netbox.py +2 -2
- osism/services/event_bridge.py +304 -0
- osism/services/listener.py +130 -19
- osism/services/websocket_manager.py +271 -0
- osism/settings.py +1 -1
- osism/tasks/conductor/ironic.py +22 -17
- osism/tasks/conductor/netbox.py +58 -1
- osism/tasks/conductor/sonic/config_generator.py +341 -26
- osism/tasks/conductor/sonic/connections.py +123 -0
- osism/tasks/conductor/sonic/interface.py +3 -1
- osism/tasks/openstack.py +35 -15
- osism/utils/__init__.py +2 -2
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/METADATA +7 -6
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/RECORD +22 -20
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/entry_points.txt +4 -0
- osism-0.20250824.0.dist-info/licenses/AUTHORS +1 -0
- osism-0.20250824.0.dist-info/pbr.json +1 -0
- osism-0.20250804.0.dist-info/licenses/AUTHORS +0 -1
- osism-0.20250804.0.dist-info/pbr.json +0 -1
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/WHEEL +0 -0
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250804.0.dist-info → osism-0.20250824.0.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,7 @@ from loguru import logger
|
|
11
11
|
|
12
12
|
from osism import utils
|
13
13
|
from osism.tasks.conductor.netbox import (
|
14
|
+
get_device_interface_ips,
|
14
15
|
get_device_loopbacks,
|
15
16
|
get_device_oob_ip,
|
16
17
|
get_device_vlans,
|
@@ -29,6 +30,7 @@ from .interface import (
|
|
29
30
|
from .connections import (
|
30
31
|
get_connected_interfaces,
|
31
32
|
get_connected_device_for_sonic_interface,
|
33
|
+
get_connected_interface_ipv4_address,
|
32
34
|
)
|
33
35
|
from .cache import get_cached_device_interfaces
|
34
36
|
|
@@ -73,6 +75,12 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None):
|
|
73
75
|
# Get Loopback configuration from NetBox
|
74
76
|
loopback_info = get_device_loopbacks(device)
|
75
77
|
|
78
|
+
# Get interface IP addresses from NetBox
|
79
|
+
interface_ips = get_device_interface_ips(device)
|
80
|
+
|
81
|
+
# Get IPv4 addresses from transfer role prefixes
|
82
|
+
transfer_ips = _get_transfer_role_ipv4_addresses(device)
|
83
|
+
|
76
84
|
# Get breakout port configuration from NetBox
|
77
85
|
breakout_info = detect_breakout_ports(device)
|
78
86
|
|
@@ -186,7 +194,14 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None):
|
|
186
194
|
)
|
187
195
|
|
188
196
|
# Add interface configurations
|
189
|
-
_add_interface_configurations(
|
197
|
+
_add_interface_configurations(
|
198
|
+
config,
|
199
|
+
connected_interfaces,
|
200
|
+
portchannel_info,
|
201
|
+
interface_ips,
|
202
|
+
netbox_interfaces,
|
203
|
+
device,
|
204
|
+
)
|
190
205
|
|
191
206
|
# Add BGP configurations
|
192
207
|
_add_bgp_configurations(
|
@@ -196,6 +211,10 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None):
|
|
196
211
|
portchannel_info,
|
197
212
|
device,
|
198
213
|
device_as_mapping,
|
214
|
+
interface_ips,
|
215
|
+
netbox_interfaces,
|
216
|
+
transfer_ips,
|
217
|
+
utils.nb,
|
199
218
|
)
|
200
219
|
|
201
220
|
# Add NTP server configuration (device-specific)
|
@@ -531,7 +550,14 @@ def _add_tagged_vlans_to_ports(config, vlan_info, netbox_interfaces, device):
|
|
531
550
|
config["PORT"][port_name]["tagged_vlans"] = tagged_vlans
|
532
551
|
|
533
552
|
|
534
|
-
def _add_interface_configurations(
|
553
|
+
def _add_interface_configurations(
|
554
|
+
config,
|
555
|
+
connected_interfaces,
|
556
|
+
portchannel_info,
|
557
|
+
interface_ips,
|
558
|
+
netbox_interfaces,
|
559
|
+
device,
|
560
|
+
):
|
535
561
|
"""Add INTERFACE configuration for connected interfaces."""
|
536
562
|
for port_name in config["PORT"]:
|
537
563
|
# Check if this port is in the connected interfaces set and not a port channel member
|
@@ -539,8 +565,148 @@ def _add_interface_configurations(config, connected_interfaces, portchannel_info
|
|
539
565
|
port_name in connected_interfaces
|
540
566
|
and port_name not in portchannel_info["member_mapping"]
|
541
567
|
):
|
542
|
-
#
|
543
|
-
|
568
|
+
# Find the NetBox interface name for this SONiC port
|
569
|
+
netbox_interface_name = None
|
570
|
+
if port_name in netbox_interfaces:
|
571
|
+
netbox_interface_name = netbox_interfaces[port_name]["netbox_name"]
|
572
|
+
|
573
|
+
# Check if this interface has an IPv4 address assigned
|
574
|
+
ipv4_address = None
|
575
|
+
if netbox_interface_name and netbox_interface_name in interface_ips:
|
576
|
+
ipv4_address = interface_ips[netbox_interface_name]
|
577
|
+
logger.info(
|
578
|
+
f"Interface {port_name} ({netbox_interface_name}) has IPv4 address: {ipv4_address}"
|
579
|
+
)
|
580
|
+
|
581
|
+
if ipv4_address:
|
582
|
+
# If IPv4 address is available, configure the interface with it
|
583
|
+
# Add base interface entry (similar to VLAN_INTERFACE and LOOPBACK_INTERFACE patterns)
|
584
|
+
config["INTERFACE"][port_name] = {}
|
585
|
+
# Add IP address suffixed entry with scope and family parameters
|
586
|
+
config["INTERFACE"][f"{port_name}|{ipv4_address}"] = {
|
587
|
+
"scope": "global",
|
588
|
+
"family": "IPv4",
|
589
|
+
}
|
590
|
+
logger.info(
|
591
|
+
f"Configured interface {port_name} with IPv4 address {ipv4_address}"
|
592
|
+
)
|
593
|
+
else:
|
594
|
+
# Add interface to INTERFACE section with ipv6_use_link_local_only enabled
|
595
|
+
config["INTERFACE"][port_name] = {"ipv6_use_link_local_only": "enable"}
|
596
|
+
logger.debug(
|
597
|
+
f"Configured interface {port_name} with IPv6 link-local only"
|
598
|
+
)
|
599
|
+
|
600
|
+
|
601
|
+
def _get_transfer_role_ipv4_addresses(device):
|
602
|
+
"""Get IPv4 addresses from IP prefixes with 'transfer' role.
|
603
|
+
|
604
|
+
Args:
|
605
|
+
device: NetBox device object
|
606
|
+
|
607
|
+
Returns:
|
608
|
+
dict: Dictionary mapping interface names to their transfer role IPv4 addresses
|
609
|
+
{
|
610
|
+
'interface_name': 'ip_address/prefix_length',
|
611
|
+
...
|
612
|
+
}
|
613
|
+
"""
|
614
|
+
transfer_ips = {}
|
615
|
+
|
616
|
+
try:
|
617
|
+
# Get all interfaces for the device
|
618
|
+
interfaces = list(utils.nb.dcim.interfaces.filter(device_id=device.id))
|
619
|
+
|
620
|
+
for interface in interfaces:
|
621
|
+
# Skip management interfaces and virtual interfaces
|
622
|
+
if interface.mgmt_only or (
|
623
|
+
hasattr(interface, "type")
|
624
|
+
and interface.type
|
625
|
+
and interface.type.value == "virtual"
|
626
|
+
):
|
627
|
+
continue
|
628
|
+
|
629
|
+
# Get IP addresses assigned to this interface
|
630
|
+
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
631
|
+
assigned_object_id=interface.id,
|
632
|
+
)
|
633
|
+
|
634
|
+
for ip_addr in ip_addresses:
|
635
|
+
if ip_addr.address:
|
636
|
+
try:
|
637
|
+
ip_obj = ipaddress.ip_interface(ip_addr.address)
|
638
|
+
if ip_obj.version == 4:
|
639
|
+
# Check if this IP belongs to a prefix with 'transfer' role
|
640
|
+
# Query for the prefix this IP belongs to
|
641
|
+
prefixes = utils.nb.ipam.prefixes.filter(
|
642
|
+
contains=str(ip_obj.ip)
|
643
|
+
)
|
644
|
+
|
645
|
+
for prefix in prefixes:
|
646
|
+
# Check if prefix has role and it's 'transfer'
|
647
|
+
if hasattr(prefix, "role") and prefix.role:
|
648
|
+
if prefix.role.slug == "transfer":
|
649
|
+
transfer_ips[interface.name] = ip_addr.address
|
650
|
+
logger.debug(
|
651
|
+
f"Found transfer role IPv4 {ip_addr.address} on interface {interface.name} of device {device.name}"
|
652
|
+
)
|
653
|
+
break
|
654
|
+
|
655
|
+
# Break after first IPv4 found (transfer or not)
|
656
|
+
if interface.name in transfer_ips:
|
657
|
+
break
|
658
|
+
except (ValueError, ipaddress.AddressValueError):
|
659
|
+
# Skip invalid IP addresses
|
660
|
+
continue
|
661
|
+
|
662
|
+
except Exception as e:
|
663
|
+
logger.warning(
|
664
|
+
f"Failed to get transfer role IPv4 addresses for device {device.name}: {e}"
|
665
|
+
)
|
666
|
+
|
667
|
+
return transfer_ips
|
668
|
+
|
669
|
+
|
670
|
+
def _has_direct_ipv4_address(port_name, interface_ips, netbox_interfaces):
|
671
|
+
"""Check if an interface has a direct IPv4 address assigned.
|
672
|
+
|
673
|
+
Args:
|
674
|
+
port_name: SONiC interface name (e.g., "Ethernet0")
|
675
|
+
interface_ips: Dict mapping NetBox interface names to IPv4 addresses
|
676
|
+
netbox_interfaces: Dict mapping SONiC names to NetBox interface info
|
677
|
+
|
678
|
+
Returns:
|
679
|
+
bool: True if interface has a direct IPv4 address, False otherwise
|
680
|
+
"""
|
681
|
+
if not interface_ips or not netbox_interfaces:
|
682
|
+
return False
|
683
|
+
|
684
|
+
if port_name in netbox_interfaces:
|
685
|
+
netbox_interface_name = netbox_interfaces[port_name]["netbox_name"]
|
686
|
+
return netbox_interface_name in interface_ips
|
687
|
+
|
688
|
+
return False
|
689
|
+
|
690
|
+
|
691
|
+
def _has_transfer_role_ipv4(port_name, transfer_ips, netbox_interfaces):
|
692
|
+
"""Check if an interface has an IPv4 from a transfer role prefix.
|
693
|
+
|
694
|
+
Args:
|
695
|
+
port_name: SONiC interface name (e.g., "Ethernet0")
|
696
|
+
transfer_ips: Dict mapping NetBox interface names to transfer role IPv4 addresses
|
697
|
+
netbox_interfaces: Dict mapping SONiC names to NetBox interface info
|
698
|
+
|
699
|
+
Returns:
|
700
|
+
bool: True if interface has a transfer role IPv4 address, False otherwise
|
701
|
+
"""
|
702
|
+
if not transfer_ips or not netbox_interfaces:
|
703
|
+
return False
|
704
|
+
|
705
|
+
if port_name in netbox_interfaces:
|
706
|
+
netbox_interface_name = netbox_interfaces[port_name]["netbox_name"]
|
707
|
+
return netbox_interface_name in transfer_ips
|
708
|
+
|
709
|
+
return False
|
544
710
|
|
545
711
|
|
546
712
|
def _add_bgp_configurations(
|
@@ -550,52 +716,189 @@ def _add_bgp_configurations(
|
|
550
716
|
portchannel_info,
|
551
717
|
device,
|
552
718
|
device_as_mapping=None,
|
719
|
+
interface_ips=None,
|
720
|
+
netbox_interfaces=None,
|
721
|
+
transfer_ips=None,
|
722
|
+
netbox=None,
|
553
723
|
):
|
554
|
-
"""Add BGP configurations.
|
724
|
+
"""Add BGP configurations.
|
725
|
+
|
726
|
+
Args:
|
727
|
+
config: Configuration dictionary to update
|
728
|
+
connected_interfaces: Set of connected interface names
|
729
|
+
connected_portchannels: Set of connected port channel names
|
730
|
+
portchannel_info: Port channel membership information
|
731
|
+
device: NetBox device object
|
732
|
+
device_as_mapping: Mapping of device names to AS numbers
|
733
|
+
interface_ips: Dict of direct IPv4 addresses on interfaces
|
734
|
+
netbox_interfaces: Dict mapping SONiC names to NetBox interface info
|
735
|
+
transfer_ips: Dict of IPv4 addresses from transfer role prefixes
|
736
|
+
netbox: NetBox API client for querying connected interface IPs
|
737
|
+
"""
|
555
738
|
# Add BGP_NEIGHBOR_AF configuration for connected interfaces
|
556
739
|
for port_name in config["PORT"]:
|
740
|
+
has_direct_ipv4 = _has_direct_ipv4_address(
|
741
|
+
port_name, interface_ips, netbox_interfaces
|
742
|
+
)
|
743
|
+
has_transfer_ipv4 = _has_transfer_role_ipv4(
|
744
|
+
port_name, transfer_ips, netbox_interfaces
|
745
|
+
)
|
746
|
+
|
557
747
|
if (
|
558
748
|
port_name in connected_interfaces
|
559
749
|
and port_name not in portchannel_info["member_mapping"]
|
560
750
|
):
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
751
|
+
# Include interfaces with transfer role IPv4 or no direct IPv4
|
752
|
+
if has_transfer_ipv4 or not has_direct_ipv4:
|
753
|
+
# Try to get the IPv4 address of the connected endpoint interface
|
754
|
+
connected_ipv4 = None
|
755
|
+
if netbox:
|
756
|
+
connected_ipv4 = get_connected_interface_ipv4_address(
|
757
|
+
device, port_name, netbox
|
758
|
+
)
|
759
|
+
|
760
|
+
# For BGP_NEIGHBOR_AF, always use interface name like IPv6 does
|
761
|
+
neighbor_id = port_name
|
762
|
+
|
763
|
+
ipv4_key = f"default|{neighbor_id}|ipv4_unicast"
|
764
|
+
config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"}
|
765
|
+
|
766
|
+
# Only add ipv6_unicast if v6only would be true (no transfer role IPv4)
|
767
|
+
if not has_transfer_ipv4:
|
768
|
+
ipv6_key = f"default|{neighbor_id}|ipv6_unicast"
|
769
|
+
config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"}
|
770
|
+
logger.debug(
|
771
|
+
f"Added BGP_NEIGHBOR_AF with ipv4_unicast and ipv6_unicast for interface {port_name} (no direct IPv4)"
|
772
|
+
)
|
773
|
+
else:
|
774
|
+
logger.debug(
|
775
|
+
f"Added BGP_NEIGHBOR_AF with ipv4_unicast only for interface {port_name} (transfer role IPv4, v6only=false)"
|
776
|
+
)
|
777
|
+
elif has_direct_ipv4 and not has_transfer_ipv4:
|
778
|
+
logger.info(
|
779
|
+
f"Excluding interface {port_name} from BGP detection (has direct IPv4 address, not transfer role)"
|
780
|
+
)
|
565
781
|
|
566
782
|
# Add BGP_NEIGHBOR_AF configuration for connected port channels
|
567
783
|
for pc_name in connected_portchannels:
|
568
|
-
|
569
|
-
|
784
|
+
# Try to get the IPv4 address of the connected endpoint interface for port channel
|
785
|
+
connected_ipv4 = None
|
786
|
+
if netbox:
|
787
|
+
connected_ipv4 = get_connected_interface_ipv4_address(
|
788
|
+
device, pc_name, netbox
|
789
|
+
)
|
790
|
+
|
791
|
+
# For BGP_NEIGHBOR_AF, always use port channel name like interfaces
|
792
|
+
neighbor_id = pc_name
|
793
|
+
|
794
|
+
ipv4_key = f"default|{neighbor_id}|ipv4_unicast"
|
795
|
+
ipv6_key = f"default|{neighbor_id}|ipv6_unicast"
|
570
796
|
config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"}
|
571
797
|
config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"}
|
572
798
|
|
573
799
|
# Add BGP_NEIGHBOR configuration for connected interfaces
|
574
800
|
for port_name in config["PORT"]:
|
801
|
+
has_direct_ipv4 = _has_direct_ipv4_address(
|
802
|
+
port_name, interface_ips, netbox_interfaces
|
803
|
+
)
|
804
|
+
has_transfer_ipv4 = _has_transfer_role_ipv4(
|
805
|
+
port_name, transfer_ips, netbox_interfaces
|
806
|
+
)
|
807
|
+
|
575
808
|
if (
|
576
809
|
port_name in connected_interfaces
|
577
810
|
and port_name not in portchannel_info["member_mapping"]
|
578
811
|
):
|
579
|
-
|
812
|
+
# Include interfaces with transfer role IPv4 or no direct IPv4
|
813
|
+
if has_transfer_ipv4 or not has_direct_ipv4:
|
814
|
+
# Try to get the IPv4 address of the connected endpoint interface
|
815
|
+
connected_ipv4 = None
|
816
|
+
if netbox:
|
817
|
+
connected_ipv4 = get_connected_interface_ipv4_address(
|
818
|
+
device, port_name, netbox
|
819
|
+
)
|
580
820
|
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
821
|
+
# Use the connected interface's IPv4 address if available, otherwise use interface name
|
822
|
+
if connected_ipv4:
|
823
|
+
neighbor_key = f"default|{connected_ipv4}"
|
824
|
+
logger.debug(
|
825
|
+
f"Using connected interface IPv4 address {connected_ipv4} for BGP neighbor on {port_name}"
|
826
|
+
)
|
827
|
+
else:
|
828
|
+
neighbor_key = f"default|{port_name}"
|
829
|
+
logger.debug(
|
830
|
+
f"No connected interface IPv4 found, using interface name {port_name} for BGP neighbor"
|
831
|
+
)
|
832
|
+
|
833
|
+
# Determine peer_type based on connected device AS
|
834
|
+
peer_type = "external" # Default
|
835
|
+
connected_device = get_connected_device_for_sonic_interface(
|
836
|
+
device, port_name
|
589
837
|
)
|
838
|
+
if connected_device:
|
839
|
+
peer_type = _determine_peer_type(
|
840
|
+
device, connected_device, device_as_mapping
|
841
|
+
)
|
590
842
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
843
|
+
# Set v6only based on whether interface has transfer role IPv4
|
844
|
+
# - Transfer role IPv4: v6only=false (dual-stack BGP)
|
845
|
+
# - No direct IPv4: v6only=true (IPv6-only BGP)
|
846
|
+
bgp_neighbor_config = {
|
847
|
+
"peer_type": peer_type,
|
848
|
+
"v6only": "false" if has_transfer_ipv4 else "true",
|
849
|
+
}
|
850
|
+
|
851
|
+
# If using IP address as key, also store the local address
|
852
|
+
if connected_ipv4:
|
853
|
+
# Get the local interface IPv4 address
|
854
|
+
local_ipv4 = None
|
855
|
+
if port_name in netbox_interfaces:
|
856
|
+
netbox_interface_name = netbox_interfaces[port_name][
|
857
|
+
"netbox_name"
|
858
|
+
]
|
859
|
+
if netbox_interface_name in interface_ips:
|
860
|
+
local_ipv4 = interface_ips[netbox_interface_name].split(
|
861
|
+
"/"
|
862
|
+
)[0]
|
863
|
+
elif netbox_interface_name in transfer_ips:
|
864
|
+
local_ipv4 = transfer_ips[netbox_interface_name].split("/")[
|
865
|
+
0
|
866
|
+
]
|
867
|
+
|
868
|
+
if local_ipv4:
|
869
|
+
bgp_neighbor_config["local_addr"] = local_ipv4
|
870
|
+
|
871
|
+
config["BGP_NEIGHBOR"][neighbor_key] = bgp_neighbor_config
|
872
|
+
|
873
|
+
if has_transfer_ipv4:
|
874
|
+
logger.debug(
|
875
|
+
f"Added BGP_NEIGHBOR for interface {port_name} (transfer role IPv4, v6only=false)"
|
876
|
+
)
|
877
|
+
else:
|
878
|
+
logger.debug(
|
879
|
+
f"Added BGP_NEIGHBOR for interface {port_name} (no direct IPv4, v6only=true)"
|
880
|
+
)
|
595
881
|
|
596
882
|
# Add BGP_NEIGHBOR configuration for connected port channels
|
597
883
|
for pc_name in connected_portchannels:
|
598
|
-
|
884
|
+
# Try to get the IPv4 address of the connected endpoint interface for port channel
|
885
|
+
connected_ipv4 = None
|
886
|
+
if netbox:
|
887
|
+
connected_ipv4 = get_connected_interface_ipv4_address(
|
888
|
+
device, pc_name, netbox
|
889
|
+
)
|
890
|
+
|
891
|
+
# Use the connected interface's IPv4 address if available, otherwise use port channel name
|
892
|
+
if connected_ipv4:
|
893
|
+
neighbor_key = f"default|{connected_ipv4}"
|
894
|
+
logger.debug(
|
895
|
+
f"Using connected interface IPv4 address {connected_ipv4} for BGP neighbor on {pc_name}"
|
896
|
+
)
|
897
|
+
else:
|
898
|
+
neighbor_key = f"default|{pc_name}"
|
899
|
+
logger.debug(
|
900
|
+
f"No connected interface IPv4 found, using port channel name {pc_name} for BGP neighbor"
|
901
|
+
)
|
599
902
|
|
600
903
|
# Determine peer_type based on connected device AS
|
601
904
|
peer_type = "external" # Default
|
@@ -605,11 +908,23 @@ def _add_bgp_configurations(
|
|
605
908
|
device, connected_device, device_as_mapping
|
606
909
|
)
|
607
910
|
|
608
|
-
|
911
|
+
bgp_neighbor_config = {
|
609
912
|
"peer_type": peer_type,
|
610
913
|
"v6only": "true",
|
611
914
|
}
|
612
915
|
|
916
|
+
# If using IP address as key, also store the local address
|
917
|
+
if connected_ipv4:
|
918
|
+
# For port channels, get the local IPv4 address from interface IPs
|
919
|
+
# Note: Port channels don't have direct IP assignments in NetBox,
|
920
|
+
# so we use the connected interface IP logic
|
921
|
+
local_ipv4 = None
|
922
|
+
# Port channels don't have NetBox interface entries,
|
923
|
+
# so we skip local_addr for port channels for now
|
924
|
+
# TODO: Implement port channel local address lookup if needed
|
925
|
+
|
926
|
+
config["BGP_NEIGHBOR"][neighbor_key] = bgp_neighbor_config
|
927
|
+
|
613
928
|
|
614
929
|
def _get_connected_device_for_interface(device, interface_name):
|
615
930
|
"""Get the connected device for a given interface name.
|
@@ -387,3 +387,126 @@ def get_device_bgp_neighbors_via_loopback(
|
|
387
387
|
logger.warning(f"Could not process BGP neighbors for device {device.name}: {e}")
|
388
388
|
|
389
389
|
return bgp_neighbors
|
390
|
+
|
391
|
+
|
392
|
+
def get_connected_interface_ipv4_address(device, sonic_port_name, netbox):
|
393
|
+
"""
|
394
|
+
Get the IPv4 address(es) of the connected endpoint interface for a given SONiC port.
|
395
|
+
Checks for direct IP addresses first, then for FHRP VIP addresses.
|
396
|
+
|
397
|
+
Args:
|
398
|
+
device: The SONiC device
|
399
|
+
sonic_port_name: The SONiC port name
|
400
|
+
netbox: The NetBox API client
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
- For direct IP: The IPv4 address string of the connected interface
|
404
|
+
- For FHRP VIP: The first VIP address found (if multiple VIPs exist, logs all but returns first)
|
405
|
+
- None if no addresses found
|
406
|
+
"""
|
407
|
+
try:
|
408
|
+
interface = netbox.dcim.interfaces.get(
|
409
|
+
device_id=device.id, name=sonic_port_name
|
410
|
+
)
|
411
|
+
if not interface:
|
412
|
+
return None
|
413
|
+
|
414
|
+
# Check if interface has connected_endpoints using the modern API
|
415
|
+
if not (
|
416
|
+
hasattr(interface, "connected_endpoints") and interface.connected_endpoints
|
417
|
+
):
|
418
|
+
return None
|
419
|
+
|
420
|
+
# Ensure connected_endpoints_reachable is True
|
421
|
+
if not getattr(interface, "connected_endpoints_reachable", False):
|
422
|
+
return None
|
423
|
+
|
424
|
+
# Process each connected endpoint to find the first valid interface
|
425
|
+
connected_interface = None
|
426
|
+
for endpoint in interface.connected_endpoints:
|
427
|
+
if hasattr(endpoint, "id"):
|
428
|
+
connected_interface = endpoint
|
429
|
+
break
|
430
|
+
|
431
|
+
if not connected_interface:
|
432
|
+
return None
|
433
|
+
|
434
|
+
# First, try to get direct IPv4 addresses assigned to the connected interface
|
435
|
+
ip_addresses = netbox.ipam.ip_addresses.filter(
|
436
|
+
assigned_object_id=connected_interface.id,
|
437
|
+
)
|
438
|
+
|
439
|
+
for ip_address in ip_addresses:
|
440
|
+
# Check if it's an IPv4 address
|
441
|
+
if "/" in str(ip_address.address):
|
442
|
+
address = str(ip_address.address).split("/")[0]
|
443
|
+
if "." in address: # IPv4 address
|
444
|
+
logger.debug(
|
445
|
+
f"Found direct IPv4 address {address} on connected interface "
|
446
|
+
f"{connected_interface.name} for port {sonic_port_name}"
|
447
|
+
)
|
448
|
+
return address
|
449
|
+
|
450
|
+
# If no direct IP found, check for FHRP group membership and VIP addresses
|
451
|
+
logger.debug(
|
452
|
+
f"No direct IPv4 found on {connected_interface.name}, checking for FHRP VIP addresses"
|
453
|
+
)
|
454
|
+
|
455
|
+
# Get FHRP group assignments for the connected interface
|
456
|
+
fhrp_assignments = netbox.ipam.fhrp_group_assignments.filter(
|
457
|
+
interface_type="dcim.interface", interface_id=connected_interface.id
|
458
|
+
)
|
459
|
+
|
460
|
+
# Get all VIP addresses once to avoid repeated API calls
|
461
|
+
try:
|
462
|
+
all_vip_addresses = netbox.ipam.ip_addresses.filter(role="vip")
|
463
|
+
except Exception as vip_e:
|
464
|
+
logger.debug(f"Could not query VIP addresses: {vip_e}")
|
465
|
+
all_vip_addresses = []
|
466
|
+
|
467
|
+
# Collect all VIP IPv4 addresses from all FHRP groups this interface belongs to
|
468
|
+
vip_addresses_found = []
|
469
|
+
|
470
|
+
for assignment in fhrp_assignments:
|
471
|
+
if not assignment.group:
|
472
|
+
continue
|
473
|
+
|
474
|
+
# Find VIP addresses assigned to this specific FHRP group
|
475
|
+
for vip in all_vip_addresses:
|
476
|
+
# Check if this VIP is assigned to the current FHRP group
|
477
|
+
if (
|
478
|
+
hasattr(vip, "assigned_object_type")
|
479
|
+
and vip.assigned_object_type == "ipam.fhrpgroup"
|
480
|
+
and hasattr(vip, "assigned_object_id")
|
481
|
+
and vip.assigned_object_id == assignment.group.id
|
482
|
+
):
|
483
|
+
# Check if it's an IPv4 address
|
484
|
+
if "/" in str(vip.address):
|
485
|
+
address = str(vip.address).split("/")[0]
|
486
|
+
if "." in address: # IPv4 address
|
487
|
+
vip_addresses_found.append(address)
|
488
|
+
logger.debug(
|
489
|
+
f"Found FHRP VIP address {address} for connected interface "
|
490
|
+
f"{connected_interface.name} (FHRP group: {assignment.group.name or assignment.group.id}) "
|
491
|
+
f"for port {sonic_port_name}"
|
492
|
+
)
|
493
|
+
|
494
|
+
# Return the first VIP address found (for BGP neighbor compatibility)
|
495
|
+
if vip_addresses_found:
|
496
|
+
logger.debug(
|
497
|
+
f"Found {len(vip_addresses_found)} VIP addresses for port {sonic_port_name}: {vip_addresses_found}"
|
498
|
+
)
|
499
|
+
logger.debug(f"Returning first VIP address: {vip_addresses_found[0]}")
|
500
|
+
return vip_addresses_found[0]
|
501
|
+
|
502
|
+
logger.debug(
|
503
|
+
f"No IPv4 address (direct or FHRP VIP) found on connected interface "
|
504
|
+
f"{connected_interface.name} for port {sonic_port_name}"
|
505
|
+
)
|
506
|
+
return None
|
507
|
+
|
508
|
+
except Exception as e:
|
509
|
+
logger.warning(
|
510
|
+
f"Could not get connected interface IPv4 for port {sonic_port_name}: {e}"
|
511
|
+
)
|
512
|
+
return None
|
@@ -697,7 +697,9 @@ def detect_breakout_ports(device):
|
|
697
697
|
)
|
698
698
|
|
699
699
|
# Calculate breakout mode
|
700
|
-
if interface_speed ==
|
700
|
+
if interface_speed == 10000 and num_subports == 4:
|
701
|
+
brkout_mode = "4x10G"
|
702
|
+
elif interface_speed == 25000 and num_subports == 4:
|
701
703
|
brkout_mode = "4x25G"
|
702
704
|
elif interface_speed == 50000 and num_subports == 4:
|
703
705
|
brkout_mode = "4x50G"
|
osism/tasks/openstack.py
CHANGED
@@ -65,23 +65,43 @@ def baremetal_node_show(self, node_id_or_name, ignore_missing=False):
|
|
65
65
|
@app.task(bind=True, name="osism.tasks.openstack.baremetal_node_list")
|
66
66
|
def baremetal_node_list(self):
|
67
67
|
conn = utils.get_openstack_connection()
|
68
|
-
|
69
|
-
result
|
68
|
+
result = conn.baremetal.nodes()
|
69
|
+
return list(result)
|
70
70
|
|
71
|
-
# Simulate the output of the OpenStack CLI with -f json and without --long
|
72
|
-
for node in nodes:
|
73
|
-
result.append(
|
74
|
-
{
|
75
|
-
"UUID": node.id,
|
76
|
-
"Name": node.name,
|
77
|
-
"Instance UUID": node.instance_id,
|
78
|
-
"Power State": node.power_state,
|
79
|
-
"Provisioning State": node.provision_state,
|
80
|
-
"Maintenance": node.is_maintenance,
|
81
|
-
}
|
82
|
-
)
|
83
71
|
|
84
|
-
|
72
|
+
def get_baremetal_nodes():
|
73
|
+
"""Get all baremetal nodes with their details.
|
74
|
+
|
75
|
+
This is a generalized function that can be used by both
|
76
|
+
CLI commands and API endpoints to retrieve baremetal node information.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
list: List of dictionaries containing node information
|
80
|
+
"""
|
81
|
+
conn = utils.get_openstack_connection()
|
82
|
+
nodes = conn.baremetal.nodes(details=True)
|
83
|
+
|
84
|
+
# Convert generator to list and extract relevant fields
|
85
|
+
node_list = []
|
86
|
+
for node in nodes:
|
87
|
+
node_info = {
|
88
|
+
"uuid": node.get("uuid"),
|
89
|
+
"name": node.get("name"),
|
90
|
+
"power_state": node.get("power_state"),
|
91
|
+
"provision_state": node.get("provision_state"),
|
92
|
+
"maintenance": node.get("maintenance"),
|
93
|
+
"instance_uuid": node.get("instance_uuid"),
|
94
|
+
"driver": node.get("driver"),
|
95
|
+
"resource_class": node.get("resource_class"),
|
96
|
+
"properties": node.get("properties", {}),
|
97
|
+
"extra": node.get("extra", {}),
|
98
|
+
"last_error": node.get("last_error"),
|
99
|
+
"created_at": node.get("created_at"),
|
100
|
+
"updated_at": node.get("updated_at"),
|
101
|
+
}
|
102
|
+
node_list.append(node_info)
|
103
|
+
|
104
|
+
return node_list
|
85
105
|
|
86
106
|
|
87
107
|
@app.task(bind=True, name="osism.tasks.openstack.baremetal_node_validate")
|
osism/utils/__init__.py
CHANGED
@@ -73,7 +73,7 @@ try:
|
|
73
73
|
)
|
74
74
|
if (
|
75
75
|
"NETBOX_TOKEN" not in secondary_nb_settings
|
76
|
-
or not secondary_nb_settings["NETBOX_TOKEN"]
|
76
|
+
or not str(secondary_nb_settings["NETBOX_TOKEN"]).strip()
|
77
77
|
):
|
78
78
|
raise ValueError(
|
79
79
|
"All NETBOX_TOKEN values in the elements of setting NETBOX_SECONDARIES need to be valid NetBox tokens"
|
@@ -82,7 +82,7 @@ try:
|
|
82
82
|
secondary_nb_list.append(
|
83
83
|
get_netbox_connection(
|
84
84
|
secondary_nb_settings["NETBOX_URL"],
|
85
|
-
secondary_nb_settings["NETBOX_TOKEN"],
|
85
|
+
str(secondary_nb_settings["NETBOX_TOKEN"]),
|
86
86
|
secondary_nb_settings.get("IGNORE_SSL_ERRORS", True),
|
87
87
|
)
|
88
88
|
)
|