Skip to content

Commit adde458

Browse files
committed
adds config options vlan_group_relation_by_name and vlan_group_relation_by_id #373
1 parent 08b244a commit adde458

File tree

8 files changed

+187
-73
lines changed

8 files changed

+187
-73
lines changed

module/config/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ def __eq__(self, other):
3030
def __contains__(self, key):
3131
return key in self.__dict__
3232

33+
def __getattr__(self, item):
34+
if item in self:
35+
return getattr(self, item)
36+
return None
3337

3438
class ConfigBase:
3539
"""

module/netbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
NBVRF,
2020
NBVLAN,
2121
NBVLANList,
22+
NBVLANGroup,
2223
NBPrefix,
2324
NBManufacturer,
2425
NBDeviceType,

module/netbox/object_classes.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,12 +1013,13 @@ def compile_vlans(self, vlans):
10131013
10141014
Parameters
10151015
----------
1016-
vlans: list of (dict, NBVLAN)
1016+
vlans: list of (dict or NBVLAN)
10171017
list of VLANs that should be in the returned list
10181018
10191019
Returns
10201020
-------
1021-
NBVLANList: of parsed VLANs
1021+
NBVLANList
1022+
of parsed VLANs
10221023
"""
10231024

10241025
if vlans is None or NBVLANList not in self.data_model.values():
@@ -1361,7 +1362,8 @@ def __init__(self, *args, **kwargs):
13611362
"site": NBSite,
13621363
"description": 200,
13631364
"tenant": NBTenant,
1364-
"tags": NBTagList
1365+
"tags": NBTagList,
1366+
"group": NBVLANGroup
13651367
}
13661368
super().__init__(*args, **kwargs)
13671369

@@ -1402,6 +1404,20 @@ def update(self, data=None, read_from_netbox=False, source=None):
14021404

14031405
super().update(data=data, read_from_netbox=read_from_netbox, source=source)
14041406

1407+
class NBVLANGroup(NetBoxObject):
1408+
name = "VLANGroup"
1409+
api_path = "ipam/vlan-groups"
1410+
primary_key = "name"
1411+
prune = False
1412+
1413+
def __init__(self, *args, **kwargs):
1414+
self.data_model = {
1415+
"name": 100,
1416+
"slug": 100,
1417+
"description": 200,
1418+
"tags": NBTagList
1419+
}
1420+
super().__init__(*args, **kwargs)
14051421

14061422
class NBVLANList(NBObjectList):
14071423
member_type = NBVLAN
Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,17 @@
1313
log = get_logger()
1414

1515

16-
class ExcludedVLAN:
17-
"""
18-
initializes and verifies if an VLAN should be excluded from being synced to NetBox
19-
"""
16+
class VLANFilter:
2017

21-
def __init__(self, vlan):
18+
def __init__(self, vlan, filter_type):
2219
self._validation_failed = False
2320

2421
self.site = None
22+
self.filter_type = filter_type
2523

26-
if vlan is None:
24+
if vlan is None or len(f"{vlan}") == 0:
2725
self._validation_failed = True
28-
log.error("submitted VLAN string for VLAN exclusion was 'None'")
26+
log.error(f"submitted VLAN {self.filter_type} string for VLAN was '{"'None'" if vlan is None else "empty" }'")
2927
return
3028

3129
vlan_split = [x.replace('\\', "") for x in re.split(r'(?<!\\)/', vlan)]
@@ -37,7 +35,7 @@ def __init__(self, vlan):
3735
self._value = vlan_split[1]
3836
else:
3937
self._validation_failed = True
40-
log.error("submitted VLAN string for VLAN exclusion contains name or site including '/'. " +
38+
log.error(f"submitted VLAN {self.filter_type} string for VLAN filter contains name or site including '/'. " +
4139
"A '/' which belongs to the name needs to be escaped like '\\/'.")
4240

4341
def site_matches(self, site_name):
@@ -49,7 +47,7 @@ def site_matches(self, site_name):
4947
# noinspection PyBroadException
5048
try:
5149
if ([self.site, site_name]).count(None) == 0 and re.search(f"^{self.site}$", site_name):
52-
log.debug2(f"VLAN exclude site name '{site_name}' matches '{self.site}'")
50+
log.debug2(f"VLAN {self.filter_type} site name '{site_name}' matches '{self.site}'")
5351
return True
5452
except Exception:
5553
return False
@@ -61,11 +59,14 @@ def is_valid(self):
6159
return not self._validation_failed
6260

6361

64-
class ExcludedVLANName(ExcludedVLAN):
62+
class FilterVLANByName(VLANFilter):
63+
"""
64+
initializes and verifies if a VLAN matches by name
65+
"""
6566

66-
def __init__(self, vlan):
67+
def __init__(self, vlan, filter_type):
6768

68-
super().__init__(vlan)
69+
super().__init__(vlan, filter_type)
6970

7071
self.name = None
7172

@@ -82,20 +83,23 @@ def matches(self, name, site=None):
8283
# string or regex matches
8384
try:
8485
if ([self.name, name]).count(None) == 0 and re.search(f"^{self.name}$", name):
85-
log.debug2(f"VLAN exclude name '{name}' matches '{self.name}'")
86+
log.debug2(f"VLAN {self.filter_type} name '{name}' matches '{self.name}'")
8687
return True
8788
except Exception as e:
88-
log.warning(f"Unable to match exclude VLAN name '{name}' to '{self.name}': {e}")
89+
log.warning(f"Unable to match {self.filter_type} VLAN name '{name}' to '{self.name}': {e}")
8990
return False
9091

9192
return False
9293

9394

94-
class ExcludedVLANID(ExcludedVLAN):
95+
class FilterVLANByID(VLANFilter):
96+
"""
97+
initializes and verifies if a VLAN matches by ID
98+
"""
9599

96-
def __init__(self, vlan):
100+
def __init__(self, vlan, filter_type):
97101

98-
super().__init__(vlan)
102+
super().__init__(vlan, filter_type)
99103

100104
self.range = None
101105

@@ -104,7 +108,7 @@ def __init__(self, vlan):
104108

105109
try:
106110
if "-" in self._value and int(self._value.split("-")[0]) >= int(self._value.split("-")[1]):
107-
log.error(f"range has to start with the lower id: {self._value}")
111+
log.error(f"VLAN {self.filter_type} range has to start with the lower ID: {self._value}")
108112
self._validation_failed = True
109113
return
110114

@@ -113,22 +117,21 @@ def __init__(self, vlan):
113117
for i in self._value.split(',')), []
114118
)
115119
except Exception as e:
116-
log.error(f"unable to extract ids from value '{self._value}': {e}")
120+
log.error(f"unable to extract VLAN IDs from value '{self._value}': {e}")
117121
self._validation_failed = True
118122

119123
def matches(self, vlan_id, site=None):
120124

121125
if self.site_matches(site) is False:
126+
log.debug2(f"VLAN {self.filter_type} site name '{site_name}' matches '{self.site}'")
122127
return False
123128

124129
try:
125130
if int(vlan_id) in self.range:
126-
log.debug2(f"VLAN exclude id '{vlan_id}' matches '{self._value}'")
131+
log.debug2(f"VLAN {self.filter_type} ID '{vlan_id}' matches '{self._value}'")
127132
return True
128133
except Exception as e:
129-
log.warning(f"Unable to match exclude VLAN id '{vlan_id}' to '{self._value}': {e}")
134+
log.warning(f"Unable to match {self.filter_type} VLAN ID '{vlan_id}' to '{self._value}': {e}")
130135
return False
131136

132137
return False
133-
134-

module/sources/common/source_base.py

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from module.netbox import *
1616
from module.common.logging import get_logger
1717
from module.common.misc import grab
18-
from module.sources.common.excluded_vlan import ExcludedVLANName, ExcludedVLANID
18+
from module.sources.common.handle_vlan import FilterVLANByName, FilterVLANByID
1919

2020
log = get_logger()
2121

@@ -247,27 +247,26 @@ def add_update_interface(self, interface_object, device_object, interface_data,
247247
248248
Parameters
249249
----------
250-
interface_object: NBVMInterface, NBInterface, None
250+
interface_object: NBVMInterface | NBInterface | None
251251
object handle of the current interface (if existent, otherwise None)
252-
device_object: NBVM, NBDevice
252+
device_object: NBVM | NBDevice
253253
device object handle this interface belongs to
254254
interface_data: dict
255255
dictionary with interface attributes to add to this interface
256256
interface_ips: list
257257
a list of ip addresses which are assigned to this interface
258-
vmware_object: (vim.HostSystem, vim.VirtualMachine)
258+
vmware_object: vim.HostSystem | vim.VirtualMachine
259259
object to add to list of objects to reevaluate
260260
261261
Returns
262262
-------
263-
objects: tuple((NBVMInterface, NBInterface), list)
263+
objects:
264+
tuple of NBVMInterface | NBInterface and list
264265
tuple with interface object that was added/updated and a list of ip address objects which were
265266
added to this interface
266267
"""
267268

268-
ip_tenant_inheritance_order = None
269-
if "ip_tenant_inheritance_order" in self.settings:
270-
ip_tenant_inheritance_order = self.settings.ip_tenant_inheritance_order
269+
ip_tenant_inheritance_order = self.settings.ip_tenant_inheritance_order
271270

272271
if not isinstance(interface_data, dict):
273272
log.error(f"Attribute 'interface_data' must be a dict() got {type(interface_data)}.")
@@ -572,7 +571,7 @@ def add_update_interface(self, interface_object, device_object, interface_data,
572571
f"untagged interface VLAN.")
573572

574573
if matching_untagged_vlan is not None:
575-
vlan_interface_data["untagged_vlan"] = matching_untagged_vlan
574+
vlan_interface_data["untagged_vlan"] = self.add_vlan_group(matching_untagged_vlan, site_name)
576575
if grab(interface_object, "data.mode") is None:
577576
vlan_interface_data["mode"] = "access"
578577

@@ -591,7 +590,7 @@ def add_update_interface(self, interface_object, device_object, interface_data,
591590
matching_tagged_vlan = None
592591

593592
if matching_tagged_vlan is not None:
594-
compiled_tagged_vlans.append(matching_tagged_vlan)
593+
compiled_tagged_vlans.append(self.add_vlan_group(matching_tagged_vlan, site_name))
595594

596595
if len(compiled_tagged_vlans) > 0:
597596
vlan_interface_data["tagged_vlans"] = compiled_tagged_vlans
@@ -633,12 +632,68 @@ def patch_data(object_to_patch, data, overwrite=False):
633632

634633
return data_to_update
635634

635+
def add_vlan_group(self, vlan_data, vlan_site) -> NBVLAN | dict:
636+
"""
637+
This function will try to find a matching VLAN group according to the settings.
638+
Name matching will take precedence over ID matching.
639+
640+
If nothing matches the input data from 'vlan_data' will be returned
641+
642+
Parameters
643+
----------
644+
vlan_data: dict | NBVLAN
645+
A dict or NBVLAN object
646+
vlan_site: str | None
647+
name of site for the VLAN
648+
649+
Returns
650+
-------
651+
NBVLAN | dict: the input vlan_data enriched with VLAN group if a match was found
652+
653+
"""
654+
655+
# get VLAN details
656+
if isinstance(vlan_data, NBVLAN):
657+
vlan_name = grab(vlan_data, "data.name")
658+
vlan_id = grab(vlan_data, "data.vid")
659+
elif isinstance(vlan_data, dict):
660+
vlan_name = vlan_data.get("name")
661+
vlan_id = vlan_data.get("vid")
662+
else:
663+
return vlan_data
664+
665+
# check existing Devices for matches
666+
log.debug2(f"Trying to find a matching VLAN Group based on the VLAN name '{vlan_name}' and VLAN ID '{vlan_id}'")
667+
668+
vlan_group = None
669+
for vlan_filter, vlan_group_name in self.settings.vlan_group_relation_by_name or list():
670+
if vlan_filter.matches(vlan_name, vlan_site):
671+
vlan_group = self.inventory.get_by_data(NBVLANGroup, data={"name": vlan_group_name})
672+
673+
if vlan_group is None:
674+
for vlan_filter, vlan_group_name in self.settings.vlan_group_relation_by_id or list():
675+
if vlan_filter.matches(vlan_id, vlan_site):
676+
vlan_group = self.inventory.get_by_data(NBVLANGroup, data={"name": vlan_group_name})
677+
678+
if vlan_group is not None:
679+
log.debug2(f"Found matching VLAN group '{vlan_group.get_display_name()}'")
680+
if isinstance(vlan_data, NBVLAN):
681+
vlan_data.update(data={"group": vlan_group})
682+
elif isinstance(vlan_data, dict):
683+
vlan_data["group"] = vlan_group
684+
else:
685+
log.debug2("No matching VLAN group found")
686+
687+
print(vlan_data)
688+
689+
return vlan_data
690+
636691
def get_vlan_object_if_exists(self, vlan_data=None, vlan_site=None):
637692
"""
638693
This function will try to find a matching VLAN object based on 'vlan_data'
639694
Will return matching objects in following order:
640-
* exact match: VLAN id and site match
641-
* global match: VLAN id matches but the VLAN has no site assigned
695+
* exact match: VLAN ID and site match
696+
* global match: VLAN ID matches but the VLAN has no site assigned
642697
If nothing matches the input data from 'vlan_data' will be returned
643698
644699
Parameters
@@ -664,10 +719,10 @@ def get_vlan_object_if_exists(self, vlan_data=None, vlan_site=None):
664719
raise ValueError("Value of 'vlan_data' needs to be a dict.")
665720

666721
# check existing Devices for matches
667-
log.debug2(f"Trying to find a {NBVLAN.name} based on the VLAN id '{vlan_data.get('vid')}'")
722+
log.debug2(f"Trying to find a {NBVLAN.name} based on the VLAN ID '{vlan_data.get('vid')}'")
668723

669724
if vlan_data.get("vid") is None:
670-
log.debug("No VLAN id set in vlan_data while trying to find matching VLAN.")
725+
log.debug("No VLAN ID set in vlan_data while trying to find matching VLAN.")
671726
return vlan_data
672727

673728
if vlan_site is None:
@@ -703,7 +758,7 @@ def get_vlan_object_if_exists(self, vlan_data=None, vlan_site=None):
703758
vlan_object_without_site.get_display_name(including_second_key=True)))
704759

705760
else:
706-
log.debug2("No matching existing VLAN found for this VLAN id.")
761+
log.debug2("No matching existing VLAN found for this VLAN ID.")
707762

708763
return return_data
709764

@@ -724,25 +779,14 @@ def add_vlan_object_to_netbox(self, vlan_data, site_name=None):
724779
725780
"""
726781

727-
# get config data
728-
disable_vlan_sync = False
729-
vlan_sync_exclude_by_name: List[ExcludedVLANName] = list()
730-
vlan_sync_exclude_by_id: List[ExcludedVLANID] = list()
731-
if "disable_vlan_sync" in self.settings:
732-
disable_vlan_sync = self.settings.disable_vlan_sync
733-
if "vlan_sync_exclude_by_name" in self.settings:
734-
vlan_sync_exclude_by_name = self.settings.vlan_sync_exclude_by_name
735-
if "vlan_sync_exclude_by_id" in self.settings:
736-
vlan_sync_exclude_by_id = self.settings.vlan_sync_exclude_by_id
737-
738782
# VLAN is already an existing NetBox VLAN, then it can be reused
739783
if isinstance(vlan_data, NetBoxObject):
740784
return True
741785

742786
if vlan_data is None:
743787
return False
744788

745-
if disable_vlan_sync is True:
789+
if self.settings.disable_vlan_sync is True:
746790
return False
747791

748792
# get VLAN details
@@ -757,11 +801,11 @@ def add_vlan_object_to_netbox(self, vlan_data, site_name=None):
757801
log.warning(f"Skipping sync of invalid VLAN '{vlan_name}' ID: '{vlan_id}'")
758802
return False
759803

760-
for excluded_vlan in vlan_sync_exclude_by_name or list():
804+
for excluded_vlan in self.settings.vlan_sync_exclude_by_name or list():
761805
if excluded_vlan.matches(vlan_name, site_name):
762806
return False
763807

764-
for excluded_vlan in vlan_sync_exclude_by_id or list():
808+
for excluded_vlan in self.settings.vlan_sync_exclude_by_id or list():
765809
if excluded_vlan.matches(vlan_id, site_name):
766810
return False
767811

0 commit comments

Comments
 (0)