Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ssot snow delete function #263

Merged
merged 6 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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] = []
jdrew82 marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 27 additions & 9 deletions nautobot_ssot/integrations/servicenow/diffsync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", []):
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -96,7 +118,7 @@ class Company(ServiceNowCRUDMixin, DiffSyncModel):
}

name: str
manufacturer: bool = False
manufacturer: bool = True

product_models: List["ProductModel"] = []

Expand All @@ -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
Expand Down Expand Up @@ -196,8 +218,6 @@ def create(cls, diffsync, ids, attrs):

return model

# TODO delete() method


class Interface(ServiceNowCRUDMixin, DiffSyncModel):
"""ServiceNow Interface model."""
Expand Down Expand Up @@ -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."""
Expand Down
6 changes: 1 addition & 5 deletions nautobot_ssot/integrations/servicenow/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down