aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHåkon Solbjørg <hakon@solbj.org>2023-04-01 18:21:18 +0200
committerHåkon Solbjørg <hakon@solbj.org>2023-04-02 09:40:57 +0200
commit79174ce9f4e9595e6641cece0d8f7783b5b5dd61 (patch)
tree1b31c9edec6d09d523edf3cff788d732553d1823
parentedd6fa640b29e3f31434f133d83aa9c96ee294ba (diff)
feat(planning2netbox): Import planning output to netbox
-rw-r--r--tools/netbox/scripts/planning2netbox/planning2netbox.py299
1 files changed, 299 insertions, 0 deletions
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)