From 55fb28647af0cfb0d5151fc8f9c540bd7432972d Mon Sep 17 00:00:00 2001 From: James Adams Date: Mon, 3 Feb 2025 15:36:39 +0000 Subject: [PATCH] netbox2aquilon: Add support for LAG interfaces LAGs or bond interface take their MAC from their child interfaces, so this requires allowing interfaces without MAC addresses as well as ensuring that the --mac argument isn't passed during their creation. --- aquilon/netbox2aquilon.py | 22 +- aquilon/scd_netbox.py | 4 +- aquilon/test_netbox2aquilon.py | 45 +- aquilon/testdata/__init__.py | 1 + .../testdata/interfaces_physical_lags.json | 573 ++++++++++++++++++ 5 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 aquilon/testdata/interfaces_physical_lags.json diff --git a/aquilon/netbox2aquilon.py b/aquilon/netbox2aquilon.py index a385402..e2e567b 100755 --- a/aquilon/netbox2aquilon.py +++ b/aquilon/netbox2aquilon.py @@ -173,16 +173,23 @@ def _netbox_copy_interfaces(self, device): if tag.slug == 'bootable': is_boot_interface = True + is_lag_interface = hasattr(interface, 'type') and interface.type.value == 'lag' + cmd = [ 'add_interface', '--machine', f'{device.aq_machine_name}', - '--mac', f'{interface.mac_address}', '--interface', f'{interface.name}', ] + if interface.mac_address and not is_lag_interface: + cmd.extend(['--mac', f'{interface.mac_address}']) + + # Specify --iftype if this is a special type of interface. + # Valid values are: bonding, bridge, loopback, management, oa, physical, public, virtual, vlan if (hasattr(interface, 'mgmt_only') and interface.mgmt_only and not is_boot_interface): # mgmt_only is only an attribute for physical devices - # Valid values are: bonding, bridge, loopback, management, oa, physical, public, virtual, vlan cmd.extend(['--iftype', 'management']) + elif is_lag_interface: + cmd.extend(['--iftype', 'bonding']) cmds.append(cmd) @@ -193,6 +200,17 @@ def _netbox_copy_interfaces(self, device): '--interface', f'{interface.name}', '--boot', ]) + + if (hasattr(interface, 'lag') and interface.lag): + # This property is a bit confusing, it contains a link to a LAG interface when the current interface + # is a member of that LAG, it does not mean that the current interface is a LAG. + cmds.append([ + 'update_interface', + '--machine', f'{device.aq_machine_name}', + '--interface', f'{interface.name}', + '--master', f'{interface.lag.name}', + ]) + return cmds def _netbox_copy_addresses(self, device): diff --git a/aquilon/scd_netbox.py b/aquilon/scd_netbox.py index 896c24e..e3c71b3 100644 --- a/aquilon/scd_netbox.py +++ b/aquilon/scd_netbox.py @@ -148,11 +148,13 @@ def get_interfaces_from_device(self, device): for interface in filter_interfaces: if interface.mac_address: interfaces.append(interface) + elif hasattr(interface, 'type') and interface.type.value == 'lag': + interfaces.append(interface) else: unusedintf += 1 if unusedintf: - logging.warning("%s interfaces without mac address were not included", unusedintf) + logging.warning("%s non-lag interfaces without mac address were not included", unusedintf) return interfaces diff --git a/aquilon/test_netbox2aquilon.py b/aquilon/test_netbox2aquilon.py index bb688b9..5fab6e4 100644 --- a/aquilon/test_netbox2aquilon.py +++ b/aquilon/test_netbox2aquilon.py @@ -55,9 +55,9 @@ def test__netbox_copy_interfaces(mocker): add_interface_base_cmd = ['add_interface', '--machine', 'system7592'] - add_bmc0 = add_interface_base_cmd + ['--mac', 'A1:B2:C3:D4:E5:3F', '--interface', 'bmc0', '--iftype', 'management'] - add_eth0 = add_interface_base_cmd + ['--mac', 'A1:B2:C3:D4:E5:DA', '--interface', 'eth0'] - add_eth1 = add_interface_base_cmd + ['--mac', 'A1:B2:C3:D4:E5:DB', '--interface', 'eth1'] + add_bmc0 = add_interface_base_cmd + ['--interface', 'bmc0', '--mac', 'A1:B2:C3:D4:E5:3F', '--iftype', 'management'] + add_eth0 = add_interface_base_cmd + ['--interface', 'eth0', '--mac', 'A1:B2:C3:D4:E5:DA'] + add_eth1 = add_interface_base_cmd + ['--interface', 'eth1', '--mac', 'A1:B2:C3:D4:E5:DB'] update_eth0 = ['update_interface', '--machine', 'system7592', '--interface', 'eth0', '--boot'] @@ -77,8 +77,8 @@ def test__netbox_copy_interfaces(mocker): aq_machine_name='system6690', ) test_obj.get_interfaces_from_device = mocker.MagicMock(return_value=deepcopy(FAKE.INTERFACES_VIRTUAL)) - add_eth0 = ['add_interface', '--machine', 'system6690', '--mac', 'A1:B2:C3:D4:E5:1B', '--interface', 'eth0'] - add_eth1 = ['add_interface', '--machine', 'system6690', '--mac', 'A1:B2:C3:D4:E5:99', '--interface', 'eth1'] + add_eth0 = ['add_interface', '--machine', 'system6690', '--interface', 'eth0', '--mac', 'A1:B2:C3:D4:E5:1B'] + add_eth1 = ['add_interface', '--machine', 'system6690', '--interface', 'eth1', '--mac', 'A1:B2:C3:D4:E5:99'] update_eth0 = ['update_interface', '--machine', 'system6690', '--interface', 'eth0', '--boot'] cmds = test_obj._netbox_copy_interfaces(fake_device) @@ -90,6 +90,37 @@ def test__netbox_copy_interfaces(mocker): # eth0 must be added before being updated assert cmds.index(add_eth0) < cmds.index(update_eth0) + + # Physical device with two bonded LAG interfaces + fake_device = SimpleNamespace( + aq_machine_name='system8211', + ) + test_obj.get_interfaces_from_device = mocker.MagicMock(return_value=deepcopy(FAKE.INTERFACES_PHYSICAL_LAGS)) + + add_interface_base_cmd = ['add_interface', '--machine', 'system8211'] + update_interface_base_cmd = ['update_interface', '--machine', 'system8211'] + + cmds = test_obj._netbox_copy_interfaces(fake_device) + + assert len(cmds) == 12 + + assert add_interface_base_cmd + ['--interface', 'bond0', '--iftype', 'bonding'] in cmds + assert add_interface_base_cmd + ['--interface', 'bond1', '--iftype', 'bonding'] in cmds + assert add_interface_base_cmd + ['--interface', 'eth0', '--mac', 'A1:B2:C3:69:2A:A1'] in cmds + assert add_interface_base_cmd + ['--interface', 'eth1', '--mac', 'A1:B2:C3:69:2A:A2'] in cmds + assert add_interface_base_cmd + ['--interface', 'eth2', '--mac', 'D4:E5:3F:52:37:8D'] in cmds + assert add_interface_base_cmd + ['--interface', 'eth3', '--mac', 'D4:E5:3F:52:37:8E'] in cmds + + assert update_interface_base_cmd + ['--interface', 'bond0', '--boot'] in cmds + assert update_interface_base_cmd + ['--interface', 'eth0', '--master', 'bond1'] in cmds + assert update_interface_base_cmd + ['--interface', 'eth1', '--master', 'bond1'] in cmds + assert update_interface_base_cmd + ['--interface', 'eth2', '--master', 'bond0'] in cmds + assert update_interface_base_cmd + ['--interface', 'eth3', '--master', 'bond0'] in cmds + + # eth0 must be added before being updated + #assert cmds.index(add_eth0) < cmds.index(update_eth0) + + def test__netbox_copy_addresses(mocker): test_obj = Netbox2Aquilon() @@ -202,14 +233,14 @@ def test__undo_cmds(): [ 'add_interface', '--machine', 'system6690', - '--mac', 'A1:B2:C3:D4:E5:1B', '--interface', 'eth0', + '--mac', 'A1:B2:C3:D4:E5:1B', ], [ 'add_interface', '--machine', 'system6690', - '--mac', 'A1:B2:C3:D4:E5:99', '--interface', 'eth1', + '--mac', 'A1:B2:C3:D4:E5:99', ], [ 'update_interface', diff --git a/aquilon/testdata/__init__.py b/aquilon/testdata/__init__.py index 96991ef..097866f 100644 --- a/aquilon/testdata/__init__.py +++ b/aquilon/testdata/__init__.py @@ -22,6 +22,7 @@ def load_data(): 'device_physical': pynetbox.models.dcim.Devices, 'device_virtual': pynetbox.models.virtualization.VirtualMachines, 'interfaces_physical': pynetbox.models.dcim.Interfaces, + 'interfaces_physical_lags': pynetbox.models.dcim.Interfaces, 'interfaces_virtual': pynetbox.core.response.Record, 'addresses_ipv4': pynetbox.models.ipam.IpAddresses, 'addresses_ipv6': pynetbox.models.ipam.IpAddresses, diff --git a/aquilon/testdata/interfaces_physical_lags.json b/aquilon/testdata/interfaces_physical_lags.json new file mode 100644 index 0000000..89f4d4c --- /dev/null +++ b/aquilon/testdata/interfaces_physical_lags.json @@ -0,0 +1,573 @@ +[ + { + "id": 35021, + "url": "https://netbox.example.org/api/dcim/interfaces/35021/?format=json", + "display": "bmc0", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "bmc0", + "label": "", + "type": { + "value": "1000base-t", + "label": "1000BASE-T (1GE)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": null, + "mtu": null, + "mac_address": "A1:B2:C3:69:2A:9B", + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": true, + "description": "", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": null, + "cable_end": "", + "wireless_link": null, + "link_peers": [], + "link_peers_type": null, + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": null, + "connected_endpoints_type": null, + "connected_endpoints_reachable": null, + "tags": [ + { + "id": 39, + "url": "https://netbox.example.org/api/extras/tags/39/?format=json", + "display": "magdb2netbox", + "name": "magdb2netbox", + "slug": "magdb2netbox", + "color": "3f51b5" + } + ], + "custom_fields": {}, + "created": "2022-11-16T00:00:00Z", + "last_updated": "2022-11-16T17:12:52.501847Z", + "count_ipaddresses": 1, + "count_fhrp_groups": 0, + "_occupied": false + }, + { + "id": 84717, + "url": "https://netbox.example.org/api/dcim/interfaces/84717/?format=json", + "display": "bond0", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "bond0", + "label": "", + "type": { + "value": "lag", + "label": "Link Aggregation Group (LAG)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": null, + "mtu": null, + "mac_address": null, + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": null, + "cable_end": "", + "wireless_link": null, + "link_peers": [], + "link_peers_type": null, + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": null, + "connected_endpoints_type": null, + "connected_endpoints_reachable": null, + "tags": [ + { + "id": 18, + "url": "https://netbox.example.org/api/extras/tags/18/?format=json", + "display": "Bootable", + "name": "Bootable", + "slug": "bootable", + "color": "ffe4e1" + } + ], + "custom_fields": {}, + "created": "2024-11-27T11:17:56.072683Z", + "last_updated": "2024-11-27T12:57:03.111733Z", + "count_ipaddresses": 1, + "count_fhrp_groups": 0, + "_occupied": false + }, + { + "id": 84718, + "url": "https://netbox.example.org/api/dcim/interfaces/84718/?format=json", + "display": "bond1", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "bond1", + "label": "", + "type": { + "value": "lag", + "label": "Link Aggregation Group (LAG)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": null, + "mtu": null, + "mac_address": null, + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": null, + "cable_end": "", + "wireless_link": null, + "link_peers": [], + "link_peers_type": null, + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": null, + "connected_endpoints_type": null, + "connected_endpoints_reachable": null, + "tags": [], + "custom_fields": {}, + "created": "2024-11-27T11:18:16.545330Z", + "last_updated": "2024-11-27T11:18:16.545350Z", + "count_ipaddresses": 1, + "count_fhrp_groups": 0, + "_occupied": false + }, + { + "id": 35022, + "url": "https://netbox.example.org/api/dcim/interfaces/35022/?format=json", + "display": "eth0", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "eth0", + "label": "", + "type": { + "value": "1000base-t", + "label": "1000BASE-T (1GE)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": { + "id": 84718, + "url": "https://netbox.example.org/api/dcim/interfaces/84718/?format=json", + "display": "bond1", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "name": "bond1", + "cable": null, + "_occupied": false + }, + "mtu": null, + "mac_address": "A1:B2:C3:69:2A:A1", + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": { + "id": 7185, + "url": "https://netbox.example.org/api/dcim/cables/7185/?format=json", + "display": "#7185", + "label": "" + }, + "cable_end": "B", + "wireless_link": null, + "link_peers": [ + { + "id": 67988, + "url": "https://netbox.example.org/api/dcim/interfaces/67988/?format=json", + "display": "swp28", + "device": { + "id": 5281, + "url": "https://netbox.example.org/api/dcim/devices/5281/?format=json", + "display": "lfsw2a.example.internal", + "name": "lfsw2a.example.internal" + }, + "name": "swp28", + "cable": 7185, + "_occupied": true + } + ], + "link_peers_type": "dcim.interface", + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": [ + { + "id": 67988, + "url": "https://netbox.example.org/api/dcim/interfaces/67988/?format=json", + "display": "swp28", + "device": { + "id": 5281, + "url": "https://netbox.example.org/api/dcim/devices/5281/?format=json", + "display": "lfsw2a.example.internal", + "name": "lfsw2a.example.internal" + }, + "name": "swp28", + "cable": 7185, + "_occupied": true + } + ], + "connected_endpoints_type": "dcim.interface", + "connected_endpoints_reachable": true, + "tags": [ + { + "id": 39, + "url": "https://netbox.example.org/api/extras/tags/39/?format=json", + "display": "magdb2netbox", + "name": "magdb2netbox", + "slug": "magdb2netbox", + "color": "3f51b5" + } + ], + "custom_fields": {}, + "created": "2022-11-16T00:00:00Z", + "last_updated": "2024-11-27T12:55:37.457290Z", + "count_ipaddresses": 0, + "count_fhrp_groups": 0, + "_occupied": true + }, + { + "id": 35023, + "url": "https://netbox.example.org/api/dcim/interfaces/35023/?format=json", + "display": "eth1", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "eth1", + "label": "", + "type": { + "value": "1000base-t", + "label": "1000BASE-T (1GE)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": { + "id": 84718, + "url": "https://netbox.example.org/api/dcim/interfaces/84718/?format=json", + "display": "bond1", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "name": "bond1", + "cable": null, + "_occupied": false + }, + "mtu": null, + "mac_address": "A1:B2:C3:69:2A:A2", + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": { + "id": 7188, + "url": "https://netbox.example.org/api/dcim/cables/7188/?format=json", + "display": "#7188", + "label": "" + }, + "cable_end": "B", + "wireless_link": null, + "link_peers": [ + { + "id": 68044, + "url": "https://netbox.example.org/api/dcim/interfaces/68044/?format=json", + "display": "swp28", + "device": { + "id": 5280, + "url": "https://netbox.example.org/api/dcim/devices/5280/?format=json", + "display": "lfsw2b.example.internal", + "name": "lfsw2b.example.internal" + }, + "name": "swp28", + "cable": 7188, + "_occupied": true + } + ], + "link_peers_type": "dcim.interface", + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": [ + { + "id": 68044, + "url": "https://netbox.example.org/api/dcim/interfaces/68044/?format=json", + "display": "swp28", + "device": { + "id": 5280, + "url": "https://netbox.example.org/api/dcim/devices/5280/?format=json", + "display": "lfsw2b.example.internal", + "name": "lfsw2b.example.internal" + }, + "name": "swp28", + "cable": 7188, + "_occupied": true + } + ], + "connected_endpoints_type": "dcim.interface", + "connected_endpoints_reachable": true, + "tags": [ + { + "id": 39, + "url": "https://netbox.example.org/api/extras/tags/39/?format=json", + "display": "magdb2netbox", + "name": "magdb2netbox", + "slug": "magdb2netbox", + "color": "3f51b5" + } + ], + "custom_fields": {}, + "created": "2022-11-16T00:00:00Z", + "last_updated": "2024-11-27T11:22:44.297488Z", + "count_ipaddresses": 0, + "count_fhrp_groups": 0, + "_occupied": true + }, + { + "id": 66523, + "url": "https://netbox.example.org/api/dcim/interfaces/66523/?format=json", + "display": "eth2", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "eth2", + "label": "", + "type": { + "value": "10gbase-x-sfpp", + "label": "SFP+ (10GE)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": { + "id": 84717, + "url": "https://netbox.example.org/api/dcim/interfaces/84717/?format=json", + "display": "bond0", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "name": "bond0", + "cable": null, + "_occupied": false + }, + "mtu": null, + "mac_address": "D4:E5:3F:52:37:8D", + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "Mezzanine card", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": null, + "cable_end": "", + "wireless_link": null, + "link_peers": [], + "link_peers_type": null, + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": null, + "connected_endpoints_type": null, + "connected_endpoints_reachable": null, + "tags": [], + "custom_fields": {}, + "created": "2023-08-01T11:24:02.779107Z", + "last_updated": "2024-11-27T11:21:34.813913Z", + "count_ipaddresses": 0, + "count_fhrp_groups": 0, + "_occupied": false + }, + { + "id": 66524, + "url": "https://netbox.example.org/api/dcim/interfaces/66524/?format=json", + "display": "eth3", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "vdcs": [], + "module": null, + "name": "eth3", + "label": "", + "type": { + "value": "10gbase-x-sfpp", + "label": "SFP+ (10GE)" + }, + "enabled": true, + "parent": null, + "bridge": null, + "lag": { + "id": 84717, + "url": "https://netbox.example.org/api/dcim/interfaces/84717/?format=json", + "display": "bond0", + "device": { + "id": 5352, + "url": "https://netbox.example.org/api/dcim/devices/5352/?format=json", + "display": "system8211", + "name": "system8211" + }, + "name": "bond0", + "cable": null, + "_occupied": false + }, + "mtu": null, + "mac_address": "D4:E5:3F:52:37:8E", + "speed": null, + "duplex": null, + "wwn": null, + "mgmt_only": false, + "description": "Mezzanine card", + "mode": null, + "rf_role": null, + "rf_channel": null, + "poe_mode": null, + "poe_type": null, + "rf_channel_frequency": null, + "rf_channel_width": null, + "tx_power": null, + "untagged_vlan": null, + "tagged_vlans": [], + "mark_connected": false, + "cable": null, + "cable_end": "", + "wireless_link": null, + "link_peers": [], + "link_peers_type": null, + "wireless_lans": [], + "vrf": null, + "l2vpn_termination": null, + "connected_endpoints": null, + "connected_endpoints_type": null, + "connected_endpoints_reachable": null, + "tags": [], + "custom_fields": {}, + "created": "2023-08-01T11:24:02.891300Z", + "last_updated": "2024-11-27T11:21:57.600871Z", + "count_ipaddresses": 0, + "count_fhrp_groups": 0, + "_occupied": false + } +]