From 80f2a9f8d71a3b3bd457992de49f6df8ed289299 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Thu, 15 Aug 2024 17:13:56 +0200 Subject: [PATCH] Add scripts for managing VLANs --- LICENSE | 1 + UNLICENSE | 24 +++++++++ vlans.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 120000 LICENSE create mode 100644 UNLICENSE create mode 100644 vlans.py diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..4761def --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +UNLICENSE \ No newline at end of file diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/vlans.py b/vlans.py new file mode 100644 index 0000000..6759c9e --- /dev/null +++ b/vlans.py @@ -0,0 +1,155 @@ +from dcim.models import Device, Interface +from ipam.models import FHRPGroup, FHRPGroupAssignment, IPAddress, Prefix, Role, VLAN, VLANGroup, VRF +from tenancy.models import Tenant +from extras.scripts import * +import netaddr + +class CreateVLANScript(Script): + class Meta: + name = 'Create VLAN' + description = 'Create and configure a new VLAN on exit switches' + scheduling_enabled = False + + vlan_name = StringVar(label='VLAN name', regex='[a-z-]', max_length=15) + vlan_id = IntegerVar(label='VLAN ID', min_value=2, max_value=4094) + tenant = ObjectVar(model=Tenant) + vrf = ObjectVar(model=VRF, label='VRF') + net4 = IPNetworkVar(label='IPv4 network', required=False, + description='IPv4 network for this VLAN') + net6 = IPNetworkVar(label='IPv6 network', required=False, + description='IPv6 network for this VLAN') + + def run(self, data, commit): + tenant = data['tenant'] + vlan_id = data['vlan_id'] + vlan_name = data['vlan_name'] + net4 = data.get('net4') + net6 = data.get('net6') + vrf = data['vrf'] + + fri_it = Tenant.objects.get(name='FRI IT') + new_net = VLANGroup.objects.get(name='new-net') + + # Get or create the VLAN. + vlan, new = VLAN.objects.get_or_create(vid=vlan_id, group=new_net) + vlan.tenant = tenant + vlan.name = vlan_name + vlan.full_clean() + vlan.save() + self.log_info(f'{"created" if new else "got"} VLAN {vlan}') + + # Get or create the FHRP group for virtual router IPs. + fhrp_group, new = FHRPGroup.objects.get_or_create(name=vlan_name, group_id=vlan_id, protocol='other') + self.log_info(f'{"created" if new else "got"} FHRP group {fhrp_group}') + + # Get or create prefixes. + prefixes = [] + for net in [net4, net6]: + if net: + prefix, new = Prefix.objects.get_or_create(prefix=net, vrf=vrf) + self.log_info(f'{"created" if new else "got"} prefix {prefix.prefix}') + prefix.tenant = tenant + prefix.vlan = vlan + prefix.tenant = tenant + prefix.role = None + prefix.full_clean() + prefix.save() + prefixes += [prefix] + + vip, new = IPAddress.objects.get_or_create(address=netaddr.IPNetwork((prefix.prefix.first+1, prefix.prefix.prefixlen)), vrf=vrf) + self.log_info(f'{"created" if new else "got"} vip {vip}') + vip.tenant = fri_it + vip.save() + fhrp_group.ip_addresses.add(vip) + + fhrp_group.full_clean() + fhrp_group.save() + + # Create or update bridge child interface on each exit. + exits = Device.objects.filter(role__slug='switch', name__startswith='exit-').order_by('name') + for index, switch in enumerate(exits): + bridge = switch.interfaces.get(name='bridge') + child, new = bridge.child_interfaces.get_or_create(device=switch, name=f'bridge.{vlan_id}') + self.log_info(f'{"created" if new else "got"} interface {child} on {switch}') + + child.type = 'virtual' + child.vrf = vrf + child.mode = 'access' + child.untagged_vlan = vlan + + fhrp_group_assignment, new = child.fhrp_group_assignments.get_or_create(group_id=fhrp_group.id, priority=0) + self.log_info(f'{"created" if new else "got"} fhrp_group_assignment {fhrp_group_assignment}') + + child.full_clean() + child.save() + + for prefix in prefixes: + network = prefix.prefix + addr_switch = netaddr.IPNetwork((network.first+2+index, network.prefixlen)) + address, new = child.ip_addresses.get_or_create(address=addr_switch) + self.log_info(f'{"created" if new else "got"} address {address}') + address.vrf = vrf + address.tenant = fri_it + address.role = '' + address.full_clean() + address.save() + + self.log_success(f'wee!') + + +class SetVLANScript(Script): + class Meta: + name = 'Set VLAN' + description = 'Set tagged and untagged VLANs on access switch ports' + fieldsets = ( + ('Ports', ('access_ports', 'switch', 'switch_ports')), + ('Settings', ('vlans', 'enable')) + ) + scheduling_enabled = False + + access_ports = MultiObjectVar(model=Device, query_params={'device_type': 'rj45-access-port'}, required=False, + description='These ports will be traced to corresponding switch ports') + switch = ObjectVar(model=Device, query_params={'role': 'switch'}, required=False, + description='Limit selection to this switch') + switch_ports = MultiObjectVar(model=Interface, query_params={'device_id': '$switch'}, required=False, + description='Select switch ports directly') + vlans = MultiObjectVar(model=VLAN, label='VLANs', required=False, + description='Select multiple VLANs to put selected ports into tagged mode') + enable = BooleanVar(label='Enable ports', default=True) + + def run(self, data, commit): + all_ports = list(data['switch_ports']) + modified_switches = set() + + # Trace doesn’t work rear ports for some reason, so do it manually. + # Assumes this layout (f=front port, r=rear port, i=interface, ---=cable): + # 1f:012.23:r1 --- 23r:panel-012:f23 --- 46i:sw-xyzzy + for device in data['access_ports']: + rearport = device.rearports.first() + panel_rearport = rearport.link_peers[0] + panel_frontport = panel_rearport.frontports.first() + all_ports += panel_frontport.link_peers + + for port in all_ports: + port.enabled = data['enable'] + match len(data['vlans']): + case 0: + port.mode = 'access' + port.tagged_vlans.clear() + port.untagged_vlan = None + case 1: + port.mode = 'access' + port.tagged_vlans.clear() + port.untagged_vlan = data['vlans'][0] + case _: + port.mode = 'tagged' + port.tagged_vlans.set(data['vlans']) + port.untagged_vlan = None + port.full_clean() + port.save() + modified_switches.add(port.device.name) + + self.log_info(f'{port.device.name} {port} is {port.mode} for {",".join(str(vlan.vid) for vlan in data["vlans"])}') + + self.log_success(f'modified switches {",".join(sorted(modified_switches))}') +