From 4c927739a1ebbfe1c884f830c4a1d99f3ea30d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Solbj=C3=B8rg?= Date: Sat, 1 Apr 2023 18:21:18 +0200 Subject: feat(planning2netbox): Import planning output to netbox --- .../scripts/planning2netbox/planning2netbox.py | 299 +++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 tools/netbox/scripts/planning2netbox/planning2netbox.py diff --git a/tools/netbox/scripts/planning2netbox/planning2netbox.py b/tools/netbox/scripts/planning2netbox/planning2netbox.py new file mode 100644 index 0000000..d96f4cb --- /dev/null +++ b/tools/netbox/scripts/planning2netbox/planning2netbox.py @@ -0,0 +1,299 @@ +from django.contrib.contenttypes.models import ContentType +from django.utils.text import slugify + +from dcim.choices import DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices, SiteStatusChoices +from dcim.models import Cable, CableTermination, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site +from extras.models import Tag +from extras.scripts import * +from ipam.models import IPAddress, Prefix, VLAN, VLANGroup, Role +from ipam.choices import PrefixStatusChoices, IPAddressFamilyChoices +from netaddr import IPNetwork +import random + +from utilities.exceptions import AbortScript + + +# Used for getting existing types/objects from Netbox. +DISTRIBUTION_SWITCH_DEVICE_ROLE = 'distribution-switch' # match the name or the slug +ROUTER_DEVICE_ROLE = 'router' +CORE_DEVICE_ROLE = 'core' +ACCESS_SWITCH_DEVICE_ROLE = DeviceRole.objects.get(name='Access Switch') +DEFAULT_SITE = Site.objects.get(slug='floor') +DEFAULT_DEVICE_TYPE = DeviceType.objects.get(model='EX2200-48T') +TAGS = [Tag.objects.get(name='dhcp-client')] +FLOOR_MGMT_VLAN = VLAN.objects.get(name="edge-mgmt.floor.r1.tele") +VLAN_GROUP_FLOOR = VLANGroup.objects.get(slug="floor") +MULTIRATE_DEVICE_TYPE = DeviceType.objects.get(model="EX4300-48MP") +CORE_DEVICE = Device.objects.get(name="r1.tele") +CORE_INTERFACE_FLOOR = Interface.objects.get(device=CORE_DEVICE, description="d1.roof") + + +# Copied from examples/tg19/netbox_tools/switchestxt2netbox.py +def parse_switches_txt(switches_txt_lines, logger): + distros = {} + switches = {} + for switch in switches_txt_lines: + # example: + # e7-1 88.92.80.0/26 2a06:5844:e:71::/64 88.92.0.66/26 2a06:5841:d:2::66/64 1071 s2.floor + switch = switch.strip().split() + if len(switch) == 0: + # skip empty lines + continue + switches[switch[0]] = { + 'sysname': switch[0], + 'subnet4': switch[1], + 'subnet6': switch[2], + 'mgmt4': switch[3], + 'mgmt6': switch[4], + 'vlan_id': int(switch[5]), + 'distro_name': switch[6], + 'is_distro': False, + 'device_type': DEFAULT_DEVICE_TYPE, + 'lag_name': "ae0", + } + return switches + +def parse_patchlist_txt(patchlist_txt_lines, switches, logger): + for patchlist in patchlist_txt_lines: + columns = patchlist.split() + switch_name = columns[0] + if 'multirate' in patchlist: + switches[switch_name]['device_type'] = MULTIRATE_DEVICE_TYPE + + uplinks = [] + links = columns[2:] + for link in links: + # Skip columns with comments + if 'ge-' in link or 'mge-' in link: + uplinks.append(link) + switches[switch_name]['uplinks'] = uplinks + + +class Planning2Netbox(Script): + + class Meta: + name = "Planning to netbox" + description = "Import output from planning into netbox" + commit_default = False + field_order = ['site_name', 'switch_count', 'switch_model'] + fieldsets = "" + + switches_txt = TextVar( + description="Switch output from planning", + ) + + patchlist_txt = TextVar( + description="Patchlist output from planning", + ) + + def run(self, data, commit): + + planning_tag, _created = Tag.objects.get_or_create(name="from-planning") + + switches_txt_lines = data['switches_txt'].split('\n') + # clean "file" content + for i in range(0, len(switches_txt_lines)-1): + switches_txt_lines[i] = switches_txt_lines[i].strip() + + patchlist_txt_lines = data['patchlist_txt'].split('\n') + # clean "file" content + for i in range(0, len(patchlist_txt_lines)-1): + patchlist_txt_lines[i] = patchlist_txt_lines[i].strip() + + switches = parse_switches_txt(switches_txt_lines, self.log_debug) + patchlist = parse_patchlist_txt(patchlist_txt_lines, switches, self.log_debug) + + self.log_info(f"Configuring {len(switches)} switches") + + self.log_info("Importing switches") + for switch_name in switches: + data = switches[switch_name] + self.log_debug(f"Creating switch {switch_name} from {data}") + switch, created_switch = Device.objects.get_or_create( + name=switch_name, + device_type=data['device_type'], + device_role=ACCESS_SWITCH_DEVICE_ROLE, + site=DEFAULT_SITE, + ) + if not created_switch: + self.log_info(f"Updating existing switch: {switch.name}") + + distro = Device.objects.get(name=data['distro_name']) + mgmt_vlan = FLOOR_MGMT_VLAN + ae_interface = None + ae_interface, created_ae_interface = Interface.objects.get_or_create( + device=switch, + name=f"{data['lag_name']}", + description=distro.name, + type=InterfaceTypeChoices.TYPE_LAG, + mode=InterfaceModeChoices.MODE_TAGGED, + ) + ae_interface.tagged_vlans.add(mgmt_vlan) + + # distro side + distro_ae_interface, created_distro_ae_interface = Interface.objects.get_or_create( + device=distro, + name=f"ae{data['vlan_id']}", # TODO: can we get this from tagged vlans on ae? + description=switch.name, + type=InterfaceTypeChoices.TYPE_LAG, + mode=InterfaceModeChoices.MODE_TAGGED, + ) + if not created_distro_ae_interface: + self.log_info(f"Updated existing distro interface: {distro_ae_interface}") + distro_ae_interface.tagged_vlans.add(mgmt_vlan) + + vlan_interface, _created_vlan_interface = Interface.objects.get_or_create( + device=switch, + name=f"vlan.{mgmt_vlan.vid}", + description=f"mgmt.{distro.name}", + type=InterfaceTypeChoices.TYPE_VIRTUAL, + mode=InterfaceModeChoices.MODE_TAGGED, + ) + + traffic_vlan, _created_traffic_vlan = VLAN.objects.get_or_create( + name=switch.name, + vid=data['vlan_id'], + group=VLAN_GROUP_FLOOR, + ) + + ae_interface.tagged_vlans.add(traffic_vlan) + ae_interface.tagged_vlans.add(traffic_vlan) + + # patchlist + switch_uplinks = data['uplinks'] + #self.log_debug(f"uplinks: {switch_uplinks}") + + # from planning we always cable from port 44 and upwards + # except for multirate then we always use 47 and 48 + # 'ge-0/0/44' or 'mge-0/0/47' + is_multirate = 'mge' in switch_uplinks[0] + uplink_port = 46 if is_multirate else 44 + uplink_port_name = "mge-0/0/{}" if is_multirate else "ge-0/0/{}" + + interface_type = ContentType.objects.get_for_model(Interface) + for distro_port in switch_uplinks: + distro_interface = Interface.objects.get( + device=distro, + name=distro_port, + ) + distro_interface.lag = distro_ae_interface + distro_interface.save() + + switch_uplink_interface = Interface.objects.get( + device=switch, + name=uplink_port_name.format(uplink_port), + ) + switch_uplink_interface.lag = ae_interface + switch_uplink_interface.save() + + # Delete A cable termination if it exists + try: + CableTermination.objects.get( + cable_end='A', + termination_id=distro_interface.id, + termination_type=interface_type, + ).delete() + except CableTermination.DoesNotExist: + pass + + # Delete B cable termination if it exists + try: + CableTermination.objects.get( + cable_end='B', + termination_id=switch_uplink_interface.id, + termination_type=interface_type, + ).delete() + except CableTermination.DoesNotExist: + pass + + # Create cable now that we've cleaned up the cable mess. + cable = Cable.objects.create() + a = CableTermination.objects.create( + cable=cable, + cable_end='A', + termination_id=distro_interface.id, + termination_type=interface_type, + ) + b = CableTermination.objects.create( + cable_end='B', + cable=cable, + termination_id=switch_uplink_interface.id, + termination_type=interface_type, + ) + cable = Cable.objects.get(id=cable.id) + # https://github.com/netbox-community/netbox/discussions/10199 + cable._terminations_modified = True + cable.save() + cable.tags.add(planning_tag) + + #self.log_debug(f"Cabled switch port {b} to distro port {a}") + + uplink_port += 1 + + + # Set mgmt ip + mgmt_addr_v4, _ = IPAddress.objects.get_or_create( + address=data['mgmt4'], + ) + mgmt_addr_v6, _ = IPAddress.objects.get_or_create( + address=data['mgmt6'], + ) + switch.primary_ip4 = mgmt_addr_v4 + switch.primary_ip6 = mgmt_addr_v6 + switch.save() + + # Set prefix + prefix_v4, _ = Prefix.objects.get_or_create( + prefix=data['subnet4'], + vlan=traffic_vlan, + ) + prefix_v6, _ = Prefix.objects.get_or_create( + prefix=data['subnet6'], + vlan=traffic_vlan, + ) + + core_subinterface, _ = Interface.objects.get_or_create( + device=CORE_DEVICE, + parent=CORE_INTERFACE_FLOOR, + name=f"{CORE_INTERFACE_FLOOR.name}.{traffic_vlan.vid}", + description=switch.name, + type=InterfaceTypeChoices.TYPE_VIRTUAL, + mode=InterfaceModeChoices.MODE_TAGGED, + ) + + # Set gw addrs + + # We "manually create" an IP address from the defined + # network (instead of from the Prefix object) + # because the Prefix is not persisted in the database yet, + # and then some of the features of it doesn't work, + # e.g. prefix.get_first_available_ip(). + + uplink_addr_v4, _ = IPAddress.objects.get_or_create( + address=IPNetwork(data['subnet4'])[1], + ) + uplink_addr_v6, _ = IPAddress.objects.get_or_create( + address=IPNetwork(data['subnet6'])[1], + ) + core_subinterface.ip_addresses.add(uplink_addr_v4) + core_subinterface.ip_addresses.add(uplink_addr_v6) + core_subinterface.tagged_vlans.add(traffic_vlan) + + # Add tag to everything we created so it's easy to identify in case we + # want to recreate + things_we_created = [ + switch, + ae_interface, + distro_ae_interface, + vlan_interface, + traffic_vlan, + prefix_v4, + prefix_v6, + mgmt_addr_v4, + mgmt_addr_v6, + uplink_addr_v4, + uplink_addr_v6, + core_subinterface, + ] + for thing in things_we_created: + thing.tags.add(planning_tag) -- cgit v1.2.3