Skip to content

Commit 8b93942

Browse files
committed
Merge branch 'sol1-468-review' into development
2 parents 19ca1bb + 7cdcf22 commit 8b93942

5 files changed

Lines changed: 201 additions & 66 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11

22
# NetBox-Sync
33

4+
> [!CAUTION]
5+
> **Maintainer wanted - sunsetting this repository by 31.10.2025 [#474](https://github.com/bb-Ricardo/netbox-sync/issues/474)**
6+
47
This is a tool to sync data from different sources to a NetBox instance.
58

69
Available source types:

module/netbox/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
NBTenant,
1818
NBSite,
1919
NBSiteGroup,
20+
NBRegion,
21+
NBLocation,
2022
NBVRF,
2123
NBVLAN,
2224
NBVLANList,

module/netbox/object_classes.py

Lines changed: 42 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,6 @@ def update(self, data=None, read_from_netbox=False, source=None):
552552

553553
parsed_data = dict()
554554
for key, value in data.items():
555-
556555
if key not in self.data_model.keys():
557556
log.error(f"Found undefined data model key '{key}' for object '{self.__class__.__name__}'")
558557
continue
@@ -751,7 +750,7 @@ def update(self, data=None, read_from_netbox=False, source=None):
751750
new_value_str = new_value_str.replace("\n", " ")
752751
log.info(f"{self.name.capitalize()} '{display_name}' attribute '{key}' changed from "
753752
f"'{current_value_str}' to '{new_value_str}'")
754-
753+
755754
self.data[key] = new_value
756755
self.updated_items.append(key)
757756
data_updated = True
@@ -1273,7 +1272,6 @@ def get_site_name(self, data=None):
12731272
if isinstance(this_site, dict):
12741273
return this_site.get("name")
12751274

1276-
12771275
class NBObjectList(list):
12781276
"""
12791277
Base class of listed NetBox objects. Extends list(). Currently used for tags and untagged VLANs
@@ -1424,39 +1422,39 @@ def __init__(self, *args, **kwargs):
14241422
super().__init__(*args, **kwargs)
14251423

14261424

1427-
# class NBLocation(NetBoxObject):
1428-
# name = "location"
1429-
# api_path = "dcim/locations"
1430-
# object_type = "dcim.location"
1431-
# primary_key = "name"
1432-
# prune = False
1433-
# read_only = True
1434-
#
1435-
# def __init__(self, *args, **kwargs):
1436-
# self.data_model = {
1437-
# "name": 100,
1438-
# "slug": 100,
1439-
# "site": NBSite,
1440-
# "tags": NBTagList
1441-
# }
1442-
# super().__init__(*args, **kwargs)
1443-
#
1444-
#
1445-
# class NBRegion(NetBoxObject):
1446-
# name = "region"
1447-
# api_path = "dcim/regions"
1448-
# object_type = "dcim.region"
1449-
# primary_key = "name"
1450-
# prune = False
1451-
# read_only = True
1452-
#
1453-
# def __init__(self, *args, **kwargs):
1454-
# self.data_model = {
1455-
# "name": 100,
1456-
# "slug": 100,
1457-
# "tags": NBTagList
1458-
# }
1459-
# super().__init__(*args, **kwargs)
1425+
class NBLocation(NetBoxObject):
1426+
name = "location"
1427+
api_path = "dcim/locations"
1428+
object_type = "dcim.location"
1429+
primary_key = "name"
1430+
prune = False
1431+
read_only = True
1432+
1433+
def __init__(self, *args, **kwargs):
1434+
self.data_model = {
1435+
"name": 100,
1436+
"slug": 100,
1437+
"site": NBSite,
1438+
"tags": NBTagList
1439+
}
1440+
super().__init__(*args, **kwargs)
1441+
1442+
1443+
class NBRegion(NetBoxObject):
1444+
name = "region"
1445+
api_path = "dcim/regions"
1446+
object_type = "dcim.region"
1447+
primary_key = "name"
1448+
prune = False
1449+
read_only = True
1450+
1451+
def __init__(self, *args, **kwargs):
1452+
self.data_model = {
1453+
"name": 100,
1454+
"slug": 100,
1455+
"tags": NBTagList
1456+
}
1457+
super().__init__(*args, **kwargs)
14601458

14611459

14621460
class NBSite(NetBoxObject):
@@ -1868,14 +1866,14 @@ class NBCluster(NetBoxObject):
18681866
api_path = "virtualization/clusters"
18691867
object_type = "virtualization.cluster"
18701868
primary_key = "name"
1871-
secondary_key = "site"
1869+
secondary_key = "scope_id"
18721870
prune = False
1873-
# include_secondary_key_if_present = True
18741871

18751872
def __init__(self, *args, **kwargs):
18761873
self.mapping = NetBoxMappings()
1874+
# scope types allowed for clusters
18771875
self.scopes = [
1878-
NBSite, NBSiteGroup
1876+
NBSite, NBSiteGroup, NBLocation, NBRegion
18791877
]
18801878
self.data_model = {
18811879
"name": 100,
@@ -1884,29 +1882,20 @@ def __init__(self, *args, **kwargs):
18841882
"tenant": NBTenant,
18851883
"group": NBClusterGroup,
18861884
"scope_type": self.mapping.scopes_object_types(self.scopes),
1887-
# currently only site is supported as a scope
1888-
"scope_id": NBSite,
1885+
# supports scoped clusters
1886+
"scope_id": NetBoxObject,
1887+
# supports pre4.2.0 clusters with site
1888+
"site": NBSite,
18891889
"tags": NBTagList
18901890
}
18911891
super().__init__(*args, **kwargs)
18921892

18931893
def update(self, data=None, read_from_netbox=False, source=None):
18941894

1895-
# Add adaption for change in NetBox 4.2.0 Device model
1896-
if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"):
1897-
if data.get("site") is not None:
1898-
data["scope_id"] = data.get("site")
1899-
data["scope_type"] = "dcim.site"
1900-
del data["site"]
1901-
1902-
if data.get("scope_id") is not None:
1903-
data["scope_type"] = "dcim.site"
1904-
19051895
super().update(data=data, read_from_netbox=read_from_netbox, source=source)
19061896

19071897
def resolve_relations(self):
1908-
1909-
self.resolve_scoped_relations("scope_id", "scope_type")
1898+
log.debug2(f"Resolving relations for {self.name} '{self.get_display_name()}'")
19101899
super().resolve_relations()
19111900

19121901

module/sources/vmware/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ def __init__(self):
143143
description="""Same as cluster site but on host level.
144144
If unset it will fall back to cluster_site_relation""",
145145
config_example="nyc02.* = New York, ffm01.* = Frankfurt"),
146+
ConfigOption("cluster_scope_type_relation",
147+
str,
148+
description="""This option defines the scope type for a cluster.
149+
The scope type can be 'dcim.site', 'dcim.sitegroup', 'dcim.location' or 'dcim.region'.
150+
This is done with a comma separated key = value list.
151+
Can be set to "<NONE>" to not assign a scope type.
152+
Note: this does not remove scope types from existing clusters in NetBox.
153+
key: defines a cluster name as regex
154+
value: defines the NetBox scope type name (use quotes if name contains commas)
155+
""",
156+
config_example="Cluster_NYC = dcim.site, Cluster_FFM = dcim.sitegroup, Cluster_BER = dcim.location"),
157+
ConfigOption("cluster_scope_id_relation",
158+
str,
159+
description="""This option defines the scope id for a cluster.
160+
The scope id is the NetBox ID of the scope type.
161+
This is done with a comma separated key = value list.
162+
To be used in combination with the 'cluster_scope_type_relation'.
163+
key: defines a cluster name as regex
164+
value: defines the NetBox scope id (use quotes if name contains commas)
165+
""",
166+
config_example="Cluster_NYC = 1, Cluster_FFM.* = 2, Cluster_BER = 7"),
146167
ConfigOption("cluster_tenant_relation",
147168
str,
148169
description="""\

module/sources/vmware/connection.py

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class VMWareHandler(SourceBase):
6262
NBDeviceRole,
6363
NBSite,
6464
NBSiteGroup,
65+
NBLocation,
66+
NBRegion,
6567
NBCluster,
6668
NBDevice,
6769
NBVM,
@@ -474,13 +476,20 @@ def get_site_name(self, object_type, object_name, cluster_name=""):
474476

475477
site_name = self.get_object_relation(object_name, relation_name)
476478

477-
if object_type == NBDevice and site_name is None:
479+
# check if cluster is in a different site than the host and override the site name if so
480+
if object_type == NBDevice:
478481
site_name = self.get_site_name(NBCluster, cluster_name)
479482
if site_name is not None:
480-
log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'")
481-
482-
# set default site name
483-
if site_name is None:
483+
log.debug2(f"Found a matching cluster site for {object_name}, using site '{site_name}'. Overriding host site relation '{relation_name}'")
484+
else:
485+
site_name = self.get_object_relation(object_name, relation_name)
486+
# set deault site name if no relation was found
487+
if site_name is None:
488+
site_name = self.site_name
489+
log.debug2(f"No site relation for {type(object_name)}: '{object_name}' found, using default site '{site_name}'")
490+
491+
# set default site name for devices
492+
if site_name is None and object_type == NBDevice:
484493
site_name = self.site_name
485494
log.debug(f"No site relation for '{object_name}' found, using default site '{site_name}'")
486495

@@ -489,8 +498,95 @@ def get_site_name(self, object_type, object_name, cluster_name=""):
489498
site_name = None
490499
log.debug2(f"Site relation for '{object_name}' set to None")
491500

501+
log.debug2(f"Returning site name '{site_name}' for {object_type.name} '{object_name}'.")
502+
492503
return site_name
504+
505+
def get_scope_type(self, object_type, object_name):
506+
"""
507+
Retrieve the scope_type for a NBCluster instance by object name or from the config option
508+
cluster_scope_type_relation
509+
510+
Note: Only NBCluster is supported as the object_type.
511+
512+
Parameters
513+
----------
514+
object_type: object type
515+
The NetBox object type (must be NBCluster).
516+
object_name: str
517+
The name of the object to look up.
518+
519+
Returns
520+
-------
521+
str or None: scope type if one is found, otherwise None
522+
"""
523+
524+
# Validate object type
525+
if object_type != NBCluster:
526+
raise ValueError(f"Object type must be '{NBCluster.name}'.")
527+
528+
# get scope type from relation config
529+
relation_name = "cluster_scope_type_relation"
530+
scope_type = self.get_object_relation(object_name, relation_name)
531+
log.debug(f"Retrieved scope type '{scope_type}' for {object_type.name} '{object_name}' from relation '{relation_name}'.")
532+
533+
# if the scope_type is a list, use the first element
534+
if scope_type is not None and type(scope_type) is list:
535+
scope_type_list = scope_type
536+
scope_type = scope_type_list[0] if len(scope_type_list) > 0 else None
537+
log.debug(f"Scope type for {object_type.name} '{object_name}' is a list, using first element: '{scope_type}'")
538+
539+
# if scope_type is not a str, return None
540+
if type(scope_type) is not str:
541+
log.debug(f"scope_type is type: {type(scope_type)}, not str")
542+
return None
543+
544+
# set scope_type to None if it is configured as "<NONE>"
545+
if scope_type == "<NONE>":
546+
log.debug(f"Scope type for {object_type.name} '{object_name}' is set to None")
547+
return None
548+
549+
log.debug2(f"Returning scope type '{scope_type}' for {object_type.name} '{object_name}'.")
550+
return scope_type
551+
552+
def get_scope_id(self, object_type, object_name):
553+
"""
554+
Retrieve the scope_id for a NBCluster instance by object name or from the config option
555+
cluster_scope_id_relation
556+
557+
Note: Only NBCluster is supported as the object_type.
493558
559+
Parameters
560+
----------
561+
object_type: type
562+
The NetBox object type (must be NBCluster).
563+
object_name: str
564+
The name of the object to look up.
565+
566+
Returns
567+
-------
568+
str or None: scope id if one is found, otherwise None
569+
"""
570+
# Validate object type
571+
if object_type != NBCluster:
572+
raise ValueError(f"Object type must be '{NBCluster.name}'.")
573+
574+
# get scope id from relation config
575+
relation_name = "cluster_scope_id_relation"
576+
scope_id = self.get_object_relation(object_name, relation_name)
577+
578+
# return None if scope_id is None or not a string
579+
if scope_id is None:
580+
log.debug(f"No scope id found for {object_name}.")
581+
return None
582+
if type(scope_id) is not str:
583+
log.debug(f"scope_id is type: {type(scope_id)}, not str")
584+
return None
585+
586+
log.debug2(f"Retrieved scope id '{scope_id}' for {object_type.name} '{object_name}' from relation '{relation_name}'. End of method.")
587+
588+
return scope_id
589+
494590
def get_object_based_on_macs(self, object_type, mac_list=None):
495591
"""
496592
Try to find a NetBox object based on list of MAC addresses.
@@ -1378,21 +1474,44 @@ def add_cluster(self, obj):
13781474
self.settings.cluster_include_filter,
13791475
self.settings.cluster_exclude_filter) is False:
13801476
return
1477+
log.debug2(f"Cluster '{name}' passes include and exclude filters. Continuing.")
13811478

1479+
# get scope type and id, or site name
1480+
scope_type = self.get_scope_type(NBCluster, full_cluster_name)
1481+
if scope_type is None:
1482+
scope_type = self.get_scope_type(NBCluster, name)
1483+
13821484
site_name = self.get_site_name(NBCluster, full_cluster_name)
13831485

1486+
scope_id = self.get_scope_id(NBCluster, full_cluster_name)
1487+
if scope_id is None:
1488+
scope_id = self.get_scope_id(NBCluster, name)
1489+
log.debug(f"Cluster '{full_cluster_name}' has scope id '{scope_id}' of type {type(scope_id)}.")
1490+
13841491
data = {
13851492
"name": name,
13861493
"type": {"name": "VMware ESXi"},
13871494
"group": group
13881495
}
13891496

13901497
if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"):
1391-
if site_name is not None:
1392-
data["scope_id"] = {"name": site_name}
1498+
# set the scope type and id if they are defined
1499+
if scope_type is not None:
1500+
data["scope_type"] = scope_type
1501+
data["scope_id"] = scope_id
1502+
log.debug(f"Cluster '{full_cluster_name}' (or {name}) has scope type '{scope_type}' "
1503+
f"and scope id '{scope_id}'.")
1504+
elif site_name is not None:
13931505
data["scope_type"] = "dcim.site"
1506+
data["scope_id"] = {"name": site_name}
1507+
else:
1508+
log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.")
13941509
else:
1395-
data["site"] = {"name": site_name}
1510+
# set site_name in the pre-4.2.0 NetBox versions if one is found
1511+
if site_name is not None:
1512+
data["site"] = {"name": site_name}
1513+
1514+
log.debug(f"Cluster '{full_cluster_name}' (or {name}) has data items '{data.items()}'.")
13961515

13971516
tenant_name = self.get_object_relation(full_cluster_name, "cluster_tenant_relation")
13981517
if tenant_name is not None:
@@ -1411,11 +1530,12 @@ def add_cluster(self, obj):
14111530
if grab(cluster_candidate, "data.name") != name:
14121531
continue
14131532

1414-
# try to find a cluster with matching site
1415-
if cluster_candidate.get_site_name() == site_name:
1416-
cluster_object = cluster_candidate
1417-
log.debug2("Found an existing cluster where 'name' and 'site' are matching")
1418-
break
1533+
if site_name is not None:
1534+
# try to find a cluster with matching site
1535+
if cluster_candidate.get_site_name() == site_name:
1536+
cluster_object = cluster_candidate
1537+
log.debug2("Found an existing cluster where 'name' and 'site' are matching")
1538+
break
14191539

14201540
if grab(cluster_candidate, "data.group") is not None and \
14211541
grab(cluster_candidate, "data.group.data.name") == group_name:

0 commit comments

Comments
 (0)