diff --git a/module/netbox/object_classes.py b/module/netbox/object_classes.py index 377c12b..4b7ec09 100644 --- a/module/netbox/object_classes.py +++ b/module/netbox/object_classes.py @@ -1904,7 +1904,7 @@ def __init__(self, *args, **kwargs): "group": NBClusterGroup, "scope_type": self.mapping.scopes_object_types(self.scopes), # supports scoped clusters - "scope_id": NetBoxObject, + "scope_id": self.scopes, # supports pre4.2.0 clusters with site "site": NBSite, "tags": NBTagList @@ -1917,6 +1917,9 @@ def update(self, data=None, read_from_netbox=False, source=None): def resolve_relations(self): log.debug2(f"Resolving relations for {self.name} '{self.get_display_name()}'") + # NetBox reports the scope as an id, turn it back into the object it points to, + # otherwise every run sees a change from the id to the object and updates the cluster + self.resolve_scoped_relations("scope_id", "scope_type") super().resolve_relations() diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 63e97d8..3b0245b 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -1628,8 +1628,11 @@ def add_cluster(self, obj): log.debug(f"Cluster '{full_cluster_name}' (or {name}) has scope type '{scope_type}' " f"and scope id '{scope_id}'.") elif site_name is not None: + # NetBox wants the id of the scoped object, so the site has to be a real + # object here. A plain dict is sent as is and rejected with + # "scope_id: A valid integer is required." data["scope_type"] = "dcim.site" - data["scope_id"] = {"name": site_name} + data["scope_id"] = self.inventory.add_update_object(NBSite, data={"name": site_name}) else: log.debug(f"Cluster '{full_cluster_name}' has no scope type or scope id.") else: diff --git a/tests/test_cluster_scope.py b/tests/test_cluster_scope.py new file mode 100644 index 0000000..5cb3a38 --- /dev/null +++ b/tests/test_cluster_scope.py @@ -0,0 +1,51 @@ +""" +A cluster scoped to a site has to carry the site object, not a name (NetBox wants the +id of the scoped object), and a scope read back from NetBox as an id has to resolve to +that object again, otherwise every run reports a change. +""" +from module.netbox.object_classes import NBCluster, NBClusterType, NBSite + + +def test_cluster_scope_is_resolved_from_an_id(inventory): + site = inventory.add_object(NBSite, data={"name": "site1"}, read_from_netbox=True) + site.nb_id = 7 + ctype = inventory.add_object(NBClusterType, data={"name": "vmware"}, read_from_netbox=True) + + # this is the shape NetBox hands back for a scoped cluster + cluster = inventory.add_object(NBCluster, data={ + "name": "c1", "type": ctype, "scope_type": "dcim.site", "scope_id": 7, + }, read_from_netbox=True) + cluster.resolve_relations() + + assert cluster.data.get("scope_id") is site, "scope was left as a plain id" + + +def test_a_site_object_survives_resolving(inventory): + site = inventory.add_object(NBSite, data={"name": "site1"}, read_from_netbox=True) + ctype = inventory.add_object(NBClusterType, data={"name": "vmware"}, read_from_netbox=True) + + cluster = inventory.add_object(NBCluster, data={ + "name": "c1", "type": ctype, "scope_type": "dcim.site", "scope_id": site, + }, read_from_netbox=True) + cluster.resolve_relations() + + assert cluster.data.get("scope_id") is site + assert cluster.data.get("scope_type") == "dcim.site", "scope type was dropped" + + +def test_source_scopes_a_cluster_with_the_site_object(vcsim, inventory, load_config, vmware_settings): + """A dict here is sent to NetBox as is and rejected with 'scope_id: A valid integer is required.'""" + from module.sources import instantiate_sources + + load_config(vmware_settings + "cluster_site_relation = .* = site1\n") + source = instantiate_sources()[0] + assert source.init_successful + inventory.resolve_relations() + source.apply() + + clusters = list(inventory.get_all_items(NBCluster)) + assert clusters, "no cluster was synced" + for cluster in clusters: + scope = cluster.data.get("scope_id") + assert isinstance(scope, NBSite), f"scope_id is {type(scope).__name__}, NetBox needs an object it can turn into an id" + assert scope.get_display_name() == "site1"