diff --git a/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py b/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py index 9a9e40a8e..a725f6ef0 100644 --- a/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py +++ b/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py @@ -4,6 +4,7 @@ import json import os +from collections import defaultdict from diffsync import DiffSync from diffsync.enum import DiffSyncFlags from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound @@ -16,6 +17,10 @@ class ServiceNowDiffSync(DiffSync): """DiffSync adapter using pysnow to communicate with a ServiceNow server.""" + # create defaultdict object to store objects that should be deleted from ServiceNow if they do not + # exist in Nautobot + objects_to_delete = defaultdict(list) + company = models.Company device = models.Device # child of location interface = models.Interface # child of device @@ -293,3 +298,23 @@ def sync_complete(self, source, diff, flags=DiffSyncFlags.NONE, logger=None): self.bulk_create_interfaces() source.tag_involved_objects(target=self) + + # If there are objects inside any of the lists in objects_to_delete then iterate over those objects + # and remove them from ServiceNow + if ( + self.objects_to_delete["interface"] + or self.objects_to_delete["device"] + or self.objects_to_delete["product_model"] + or self.objects_to_delete["location"] + or self.objects_to_delete["company"] + ): + for grouping in ( + "interface", + "device", + "product_model", + "location", + "company", + ): + for sn_object in self.objects_to_delete[grouping]: + sn_object.delete() + self.objects_to_delete[grouping] = [] diff --git a/nautobot_ssot/integrations/servicenow/diffsync/models.py b/nautobot_ssot/integrations/servicenow/diffsync/models.py index e17ba9323..490f2c601 100644 --- a/nautobot_ssot/integrations/servicenow/diffsync/models.py +++ b/nautobot_ssot/integrations/servicenow/diffsync/models.py @@ -15,7 +15,7 @@ class ServiceNowCRUDMixin: _sys_id_cache = {} """Dict of table -> column_name -> value -> sys_id.""" - def map_data_to_sn_record(self, data, mapping_entry, existing_record=None): + def map_data_to_sn_record(self, data, mapping_entry, existing_record=None, clear_cache=False): """Map create/update data from DiffSync to a corresponding ServiceNow data record.""" record = existing_record or {} for mapping in mapping_entry.get("mappings", []): @@ -31,6 +31,9 @@ def map_data_to_sn_record(self, data, mapping_entry, existing_record=None): raise NotImplementedError column_name = mapping["reference"]["column"] if value is not None: + # if clear_cache is set to True then clear the cache for the object + if clear_cache: + self._sys_id_cache.setdefault(tablename, {}).setdefault(column_name, {})[value] = {} # Look in the cache first sys_id = self._sys_id_cache.get(tablename, {}).get(column_name, {}).get(value, None) if not sys_id: @@ -40,7 +43,6 @@ def map_data_to_sn_record(self, data, mapping_entry, existing_record=None): else: sys_id = target["sys_id"] self._sys_id_cache.setdefault(tablename, {}).setdefault(column_name, {})[value] = sys_id - record[mapping["reference"]["key"]] = sys_id else: raise NotImplementedError @@ -82,7 +84,27 @@ def update(self, attrs): super().update(attrs) return self - # TODO delete() method + def delete(self): + """Delete an existing instance in ServiceNow if it does not exist in Nautobot. This code adds the ServiceNow object to the objects_to_delete dict of lists. The actual delete occurs in the post-run method of adapter_servicenow.py.""" + entry = self.diffsync.mapping_data[self.get_type()] + sn_resource = self.diffsync.client.resource(api_path=f"/table/{entry['table']}") + query = self.map_data_to_sn_record(data=self.get_identifiers(), mapping_entry=entry) + try: + sn_resource.get(query=query).one() + except pysnow.exceptions.MultipleResults: + self.diffsync.job.logger.error( + f"Unsure which record to update, as query {query} matched more than one item " + f"in table {entry['table']}" + ) + return None + self.diffsync.job.logger.warning(f"{self._modelname} {self.get_identifiers()} will be deleted.") + _object = sn_resource.get(query=query) + self.diffsync.objects_to_delete[self._modelname].append(_object) + self.map_data_to_sn_record( + data=self.get_identifiers(), mapping_entry=entry, clear_cache=True + ) # remove device cache + super().delete() + return self class Company(ServiceNowCRUDMixin, DiffSyncModel): @@ -96,7 +118,7 @@ class Company(ServiceNowCRUDMixin, DiffSyncModel): } name: str - manufacturer: bool = False + manufacturer: bool = True product_models: List["ProductModel"] = [] @@ -110,7 +132,7 @@ class ProductModel(ServiceNowCRUDMixin, DiffSyncModel): _modelname = "product_model" _identifiers = ("manufacturer_name", "model_name", "model_number") - manufacturer_name: Optional[str] # some ServiceNow products have no associated manufacturer? + manufacturer_name: str # Nautobot has only one combined "model" field, but ServiceNow has both name and number model_name: str model_number: str @@ -196,8 +218,6 @@ def create(cls, diffsync, ids, attrs): return model - # TODO delete() method - class Interface(ServiceNowCRUDMixin, DiffSyncModel): """ServiceNow Interface model.""" @@ -253,8 +273,6 @@ def create(cls, diffsync, ids, attrs): model = super().create(diffsync, ids=ids, attrs=attrs) return model - # TODO delete() method - class IPAddress(ServiceNowCRUDMixin, DiffSyncModel): """An IPv4 or IPv6 address.""" diff --git a/nautobot_ssot/integrations/servicenow/jobs.py b/nautobot_ssot/integrations/servicenow/jobs.py index e69da0c4b..f13fb3706 100644 --- a/nautobot_ssot/integrations/servicenow/jobs.py +++ b/nautobot_ssot/integrations/servicenow/jobs.py @@ -22,11 +22,7 @@ class ServiceNowDataTarget(DataTarget, Job): # pylint: disable=abstract-method debug = BooleanVar(description="Enable for more verbose logging.") - # TODO: not yet implemented - # delete_records = BooleanVar( - # description="Delete records from ServiceNow if not present in Nautobot", - # default=False, - # ) + delete_records = BooleanVar(description="Delete synced records from ServiceNow if not present in Nautobot") site_filter = ObjectVar( description="Only sync records belonging to a single Site.",