From e29a643eab7fe8648dbca411dd4d9517fd9fadcb Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 17 May 2021 15:58:11 -0400 Subject: [PATCH 01/42] Initial content creation --- nautobot_data_sync/__init__.py | 2 +- nautobot_data_sync/choices.py | 33 +++++ nautobot_data_sync/filters.py | 31 ++++ nautobot_data_sync/forms.py | 42 ++++++ nautobot_data_sync/migrations/0001_initial.py | 54 +++++++ nautobot_data_sync/models.py | 105 +++++++++++++ nautobot_data_sync/navigation.py | 18 +++ nautobot_data_sync/sync/__init__.py | 96 ++++++++++++ nautobot_data_sync/sync/base.py | 139 ++++++++++++++++++ nautobot_data_sync/sync/example.py | 40 +++++ nautobot_data_sync/tables.py | 135 +++++++++++++++++ .../templates/nautobot_data_sync/sync.html | 39 +++++ .../nautobot_data_sync/sync_detail.html | 73 +++++++++ .../nautobot_data_sync/sync_home.html | 115 +++++++++++++++ nautobot_data_sync/urls.py | 17 ++- nautobot_data_sync/views.py | 138 +++++++++++++++++ poetry.lock | 88 ++++++++++- pyproject.toml | 4 + 18 files changed, 1161 insertions(+), 8 deletions(-) create mode 100644 nautobot_data_sync/choices.py create mode 100644 nautobot_data_sync/filters.py create mode 100644 nautobot_data_sync/forms.py create mode 100644 nautobot_data_sync/migrations/0001_initial.py create mode 100644 nautobot_data_sync/models.py create mode 100644 nautobot_data_sync/navigation.py create mode 100644 nautobot_data_sync/sync/__init__.py create mode 100644 nautobot_data_sync/sync/base.py create mode 100644 nautobot_data_sync/sync/example.py create mode 100644 nautobot_data_sync/tables.py create mode 100644 nautobot_data_sync/templates/nautobot_data_sync/sync.html create mode 100644 nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html create mode 100644 nautobot_data_sync/templates/nautobot_data_sync/sync_home.html create mode 100644 nautobot_data_sync/views.py diff --git a/nautobot_data_sync/__init__.py b/nautobot_data_sync/__init__.py index 404464ec5..0f77cab45 100644 --- a/nautobot_data_sync/__init__.py +++ b/nautobot_data_sync/__init__.py @@ -12,7 +12,7 @@ class NautobotDataSyncConfig(PluginConfig): verbose_name = "Nautobot Data Sync" version = __version__ author = "Network to Code, LLC" - description = "Nautobot Data Sync." + description = "Nautobot Data Sync" base_url = "data-sync" required_settings = [] min_version = "1.0.1" diff --git a/nautobot_data_sync/choices.py b/nautobot_data_sync/choices.py new file mode 100644 index 000000000..a983e537d --- /dev/null +++ b/nautobot_data_sync/choices.py @@ -0,0 +1,33 @@ +"""ChoiceSet classes for data synchronization.""" + +from nautobot.utilities.choices import ChoiceSet + + +class SyncLogEntryActionChoices(ChoiceSet): + """Valid values for a SyncLogEntry.action field.""" + + ACTION_NO_CHANGE = "no-change" + ACTION_CREATE = "create" + ACTION_UPDATE = "update" + ACTION_DELETE = "delete" + + CHOICES = ( + (ACTION_NO_CHANGE, "no change"), + (ACTION_CREATE, "create"), + (ACTION_UPDATE, "update"), + (ACTION_DELETE, "delete"), + ) + + +class SyncLogEntryStatusChoices(ChoiceSet): + """Valid values for a SyncLogEntry.status field.""" + + STATUS_SUCCESS = "success" + STATUS_FAILURE = "failure" + STATUS_ERROR = "error" + + CHOICES = ( + (STATUS_SUCCESS, "succeeded"), + (STATUS_FAILURE, "failed"), + (STATUS_ERROR, "errored"), + ) diff --git a/nautobot_data_sync/filters.py b/nautobot_data_sync/filters.py new file mode 100644 index 000000000..76f87dfb2 --- /dev/null +++ b/nautobot_data_sync/filters.py @@ -0,0 +1,31 @@ +"""Filtering logic for Sync and SyncLogEntry records.""" + +import django_filters +from django.db.models import Q + +from nautobot.utilities.filters import BaseFilterSet + +from .models import Sync, SyncLogEntry + + +class SyncFilter(BaseFilterSet): + """Filter capabilities for SyncOverview instances.""" + + class Meta: + model = Sync + fields = ["dry_run", "job_result"] + + +class SyncLogEntryFilter(BaseFilterSet): + """Filter capabilities for SyncLogEntry instances.""" + + q = django_filters.CharFilter(method="search", label="Search") + + class Meta: + model = SyncLogEntry + fields = ["sync", "action", "status", "changed_object_type"] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(Q(diff__icontains=value) | Q(message_icontains=value)) diff --git a/nautobot_data_sync/forms.py b/nautobot_data_sync/forms.py new file mode 100644 index 000000000..0ccb384a9 --- /dev/null +++ b/nautobot_data_sync/forms.py @@ -0,0 +1,42 @@ +"""Forms for working with Sync and SyncLogEntry models.""" + +from django import forms + +from nautobot.utilities.forms import add_blank_choice, BootstrapMixin, BOOLEAN_WITH_BLANK_CHOICES + +from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from .models import Sync, SyncLogEntry + + +class SyncFilterForm(BootstrapMixin, forms.ModelForm): + """Form for filtering SyncOverview records.""" + + dry_run = forms.ChoiceField(choices=BOOLEAN_WITH_BLANK_CHOICES, required=False) + + class Meta: + model = Sync + fields = ["dry_run"] + + +class SyncLogEntryFilterForm(BootstrapMixin, forms.ModelForm): + """Form for filtering SyncLogEntry records.""" + + q = forms.CharField(required=False, label="Search") + sync = forms.ModelChoiceField(queryset=Sync.objects.all(), required=False) + action = forms.ChoiceField(choices=add_blank_choice(SyncLogEntryActionChoices), required=False) + status = forms.ChoiceField(choices=add_blank_choice(SyncLogEntryStatusChoices), required=False) + + class Meta: + model = SyncLogEntry + fields = ["sync", "action", "status"] + + +class SyncForm(BootstrapMixin, forms.Form): + """Base class for dynamic form generation for a SyncWorker.""" + + dry_run = forms.BooleanField( + required=False, + initial=True, + label="Dry run", + help_text="Perform a dry run, making no actual changes to the database.", + ) diff --git a/nautobot_data_sync/migrations/0001_initial.py b/nautobot_data_sync/migrations/0001_initial.py new file mode 100644 index 000000000..577e105a3 --- /dev/null +++ b/nautobot_data_sync/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 3.1.11 on 2021-05-17 18:39 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('extras', '0005_configcontext_device_types'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Sync', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('_custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('dry_run', models.BooleanField(default=False)), + ('diff', models.JSONField()), + ('job_result', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='extras.jobresult')), + ], + options={ + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='SyncLogEntry', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('action', models.CharField(max_length=32)), + ('status', models.CharField(max_length=32)), + ('diff', models.JSONField()), + ('changed_object_id', models.UUIDField(blank=True, null=True)), + ('object_repr', models.CharField(editable=False, max_length=200)), + ('message', models.CharField(blank=True, max_length=511)), + ('changed_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')), + ('object_change', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='extras.objectchange')), + ('sync', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', related_query_name='log', to='nautobot_data_sync.sync')), + ], + options={ + 'verbose_name_plural': 'sync log entries', + 'ordering': ['sync', 'timestamp'], + }, + ), + ] diff --git a/nautobot_data_sync/models.py b/nautobot_data_sync/models.py new file mode 100644 index 000000000..9a4b4d097 --- /dev/null +++ b/nautobot_data_sync/models.py @@ -0,0 +1,105 @@ +""" +Django Models for recording the status and progress of data synchronization between data sources. + +The interaction between these models and Nautobot's native JobResult model deserves some examination. + +- A JobResult is created each time a data sync is requested. + - This stores a reference to the specific sync operation requested (JobResult.name), + much as a Job-related JobResult would reference the name of the Job. + - This stores a 'job_id', which this plugin uses to reference the specific sync instance. + - This stores the 'created' and 'completed' timestamps, and the requesting user (if any) + - This stores the overall 'status' of the job (pending, running, completed, failed, errored.) + - This stores a 'data' field which, in theory can store arbitrary JSON data, but in practice + expects a fairly strict structure for logging of various status messages. + This field is therefore not suitable for storage of in-depth data synchronization log messages, + which have a different set of content requirements, but is used for high-level status reporting. + +JobResult 1-->1 Sync 1-->n SyncLogEntry 1-->1 ObjectChange +""" + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.urls import reverse + +from nautobot.core.models import BaseModel +from nautobot.extras.models import ChangeLoggedModel, CustomFieldModel, JobResult, ObjectChange, RelationshipModel + +from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices + + +class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): + """High-level overview of a data sync event/process/attempt. + + Essentially an extension of the JobResult model to add a few additional fields. + """ + + dry_run = models.BooleanField( + default=False, help_text="Report what data would be synced but do not make any changes" + ) + diff = models.JSONField() + job_result = models.ForeignKey(to=JobResult, on_delete=models.PROTECT, blank=True, null=True) + + class Meta: + ordering = ["-created"] + + def get_absolute_url(self): + return reverse("plugins:nautobot_data_sync:sync", kwargs={"pk": self.pk}) + + +class SyncLogEntry(BaseModel): + """Record of a single event during a data sync operation. + + Detailed sync logs are recorded in this model, rather than in JobResult.data, because + JobResult.data imposes fairly strict expectations about the structure of its contents + that do not align well with the requirements of this plugin. + + This model somewhat "shadows" Nautobot's built-in ObjectChange model; the key distinction to + bear in mind is that an ObjectChange reflects a change that *did happen*, while a SyncLogEntry + may reflect this or may reflect a change that *could not happen* or *failed*. + """ + + sync = models.ForeignKey( + to=Sync, on_delete=models.CASCADE, related_name="logs", related_query_name="log" + ) + timestamp = models.DateTimeField(auto_now_add=True) + + action = models.CharField(max_length=32, choices=SyncLogEntryActionChoices) + status = models.CharField(max_length=32, choices=SyncLogEntryStatusChoices) + diff = models.JSONField() + + changed_object_type = models.ForeignKey( + to=ContentType, blank=True, null=True, on_delete=models.PROTECT, + ) + changed_object_id = models.UUIDField(blank=True, null=True) + changed_object = GenericForeignKey(ct_field="changed_object_type", fk_field="changed_object_id") + + object_repr = models.CharField(max_length=200, editable=False) + object_change = models.ForeignKey(to=ObjectChange, on_delete=models.SET_NULL, blank=True, null=True) + + message = models.CharField(max_length=511, blank=True) + + @property + def dry_run(self): + return self.overview.dry_run + + class Meta: + verbose_name_plural = "sync log entries" + ordering = ["sync", "timestamp"] + + def get_action_class(self): + """Map self.action to a Bootstrap label class.""" + return { + SyncLogEntryActionChoices.ACTION_NO_CHANGE: "default", + SyncLogEntryActionChoices.ACTION_CREATE: "success", + SyncLogEntryActionChoices.ACTION_UPDATE: "info", + SyncLogEntryActionChoices.ACTION_DELETE: "warning", + }.get(self.action) + + def get_status_class(self): + """Map self.status to a Bootstrap label class.""" + return { + SyncLogEntryStatusChoices.STATUS_SUCCESS: "success", + SyncLogEntryStatusChoices.STATUS_FAILURE: "warning", + SyncLogEntryStatusChoices.STATUS_ERROR: "danger", + }.get(self.status) diff --git a/nautobot_data_sync/navigation.py b/nautobot_data_sync/navigation.py new file mode 100644 index 000000000..6b6e28bc0 --- /dev/null +++ b/nautobot_data_sync/navigation.py @@ -0,0 +1,18 @@ +"""Plugin additions to the Nautobot navigation menu.""" + +from nautobot.extras.plugins import PluginMenuItem, PluginMenuButton +from nautobot.utilities.choices import ButtonColorChoices + + +menu_items = ( + PluginMenuItem( + link="plugins:nautobot_data_sync:sync_list", + link_text="Data Syncs", + permissions=["nautobot_data_sync.view_sync"], + ), + PluginMenuItem( + link="plugins:nautobot_data_sync:synclogentry_list", + link_text="Data Sync Detailed Logs", + permissions=["nautobot_data_sync.view_synclogentry"], + ), +) diff --git a/nautobot_data_sync/sync/__init__.py b/nautobot_data_sync/sync/__init__.py new file mode 100644 index 000000000..0d6c9a86e --- /dev/null +++ b/nautobot_data_sync/sync/__init__.py @@ -0,0 +1,96 @@ +"""Worker code for executing DataSyncWorkers.""" + +import logging + +import pkg_resources + +from django.utils import timezone + +from django_rq import job +import structlog + +from nautobot.extras.choices import JobResultStatusChoices + +from nautobot_data_sync.choices import SyncLogEntryActionChoices +from nautobot_data_sync.models import Sync, SyncLogEntry + + +logger = logging.getLogger("rq.worker") + + +def log_to_log_entry(_logger, _log_method, event_dict): + """Capture certain structlog messages from DiffSync into the Nautobot database.""" + if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): + sync = event_dict["src"].sync + sync_worker = event_dict["src"].sync_worker + object_repr = event_dict["unique_id"] + # The DiffSync log gives us a model name (string) and unique_id (string). + # Try to look up the actual Nautobot object that this describes. + changed_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) + if changed_object: + object_repr = repr(changed_object) + SyncLogEntry.objects.create( + sync=sync, + action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, + diff=event_dict["diffs"], + status=event_dict["status"], + message=event_dict["event"], + changed_object=changed_object, + object_change=object_change, + object_repr=object_repr, + ) + return event_dict + + +def get_sync_worker_classes(): + return [ + entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_data_sync.sync_worker_classes") + ] + + +def get_sync_worker_class(name=None, slug=None): + """Look up the specified sync worker class.""" + for entrypoint in pkg_resources.iter_entry_points("nautobot_data_sync.sync_worker_classes"): + sync_worker_class = entrypoint.load() + if name and sync_worker_class.name == name: + return sync_worker_class + if slug and sync_worker_class.slug == slug: + return sync_worker_class + else: + raise KeyError(f'No registered sync worker "{name or slug}" found!') + + +@job("default") +def sync(sync_id, data): + """Perform a requested sync.""" + sync = Sync.objects.get(id=sync_id) + + logger.info("START: data synchronization %s", sync) + + sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) + sync.job_result.save() + + try: + structlog.configure( + processors=[log_to_log_entry, structlog.stdlib.render_to_log_kwargs,], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + sync_worker = get_sync_worker_class(name=sync.job_result.name)(sync=sync, data=data) + + sync_worker.execute(dry_run=sync.dry_run) + + except Exception as exc: + logger.error("Exception occurred during %s: %s", sync, exc) + sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) + else: + logger.info("FINISH: data synchronization %s", sync) + sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) + finally: + sync.job_result.completed = timezone.now() + sync.job_result.save() + + return {"ok": sync.job_result.status == JobResultStatusChoices.STATUS_COMPLETED} diff --git a/nautobot_data_sync/sync/base.py b/nautobot_data_sync/sync/base.py new file mode 100644 index 000000000..f9b7eaea6 --- /dev/null +++ b/nautobot_data_sync/sync/base.py @@ -0,0 +1,139 @@ +"""Base/generic functionality for implementation of a data synchronization worker class.""" + +from collections import OrderedDict + +from django.utils.functional import classproperty + +from nautobot.extras.choices import LogLevelChoices +from nautobot.extras.jobs import ScriptVariable + +from nautobot_data_sync.forms import SyncForm +from nautobot_data_sync.models import SyncLogEntry + + +class DataSyncWorker: + """Semi-abstract base class to serve as a parent for all data sync worker implementations.""" + + def __init__(self, sync=None, data=None): + """Instantiate a DataSyncWorker in preparation for executing the data sync. + + Args: + sync (Sync): Database object that will be used to track the progress of the data sync. + data (dict): Key-value pairs of parameters passed into this worker + """ + self.sync = sync + self.data = data + + class Meta: + """Metaclass attributes of a DataSyncWorker. + + Fields that can be defined here, and their default values if undefined, include: + - dry_run_default (True) - Default value for "dry_run" when rendering as a form. + - name (cls.__name__) - Short name of this worker class. + - slug (cls.__name__) - URL-safe unique identifier of this worker class. + - description ("")- Detailed description of this worker class. + """ + dry_run_default = True + + @classproperty + def name(cls): + """Short name of this worker.""" + return getattr(cls.Meta, "name", cls.__name__) + + @classproperty + def slug(cls): + """URL-safe unique identifier of this worker.""" + return getattr(cls.Meta, "slug", cls.__name__.lower()) + + @classproperty + def description(cls): + """Detailed description of this worker.""" + return getattr(cls.Meta, "description", "") + + @classmethod + def _get_vars(cls): + """Get the dictionary of ScriptVariable attributes on this class.""" + vars = OrderedDict() + for name, attr in cls.__dict__.items(): + if isinstance(attr, ScriptVariable): + vars[name] = attr + return vars + + @classmethod + def as_form(cls, *args, **kwargs): + """Construct a Django form suitable for providing any input parameters required. + + args and kwargs are passed through to the Form constructor. + + Heavily based on nautobot.extras.jobs.Job.as_form(). + """ + fields = {name: var.as_field() for name, var in cls._get_vars().items()} + # Create a new Form class inheriting from SyncForm with fields as its attributes + FormClass = type("SyncForm", (SyncForm,), fields) + # Instantiate the Form class + form = FormClass(*args, **kwargs) + + # Set initial value of "dry run" checkbox + form.fields["dry_run"].initial = getattr(cls.Meta, "dry_run_default", True) + + return form + + @property + def job_result(self): + return self.sync.job_result + + def job_log( + self, + message, + object=None, + level=LogLevelChoices.LOG_DEFAULT, + grouping="sync", + logger=None, + ): + """Log a status message to the JobResult record.""" + self.job_result.log(message, obj=object, level_choice=level, grouping=grouping, logger=logger) + + def sync_log( + self, + action, + status, + diff=None, + changed_object=None, + object_repr=None, + object_change=None, + ): + """Log a action message as a SyncLogEntry.""" + if not diff: + diff = {} + if changed_object and not object_repr: + object_repr = repr(changed_object) + + SyncLogEntry.objects.create( + sync=self.sync, + action=action, + status=status, + message=message, + diff=diff, + changed_object=changed_object, + object_repr=object_repr, + object_change=object_change, + ) + + # + # Methods to be implemented by subclasses, below: + # + + def execute(self, dry_run=True): + """Perform a dry run or actual data synchronization.""" + + def lookup_object(self, model_name, unique_id): + """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. + + Args: + model_name (str): DiffSyncModel class name or similar class/model label. + unique_id (str): DiffSyncModel unique_id or similar unique identifier. + + Returns: + tuple: (nautobot_record, nautobot_objectchange_record). Either or both may be None. + """ + return (None, None) diff --git a/nautobot_data_sync/sync/example.py b/nautobot_data_sync/sync/example.py new file mode 100644 index 000000000..8461d5152 --- /dev/null +++ b/nautobot_data_sync/sync/example.py @@ -0,0 +1,40 @@ +"""Example implementation of a Nautobot Data Sync worker class.""" + +from django.db import transaction + +from nautobot.dcim.models import Site, RackGroup, Rack +from nautobot.extras.jobs import StringVar +from nautobot.utilities.exceptions import AbortTransaction + +from nautobot_data_sync.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from nautobot_data_sync.sync.base import DataSyncWorker + +class ExampleSyncWorker(DataSyncWorker): + + site_slug = StringVar(description="Which site's data to synchronize", default="") + + class Meta: + name = "Example Sync Worker" + slug = "example-sync-worker" + description = "An example of how a sync worker might be implemented" + + def execute(self, dry_run=True): + """Perform a dry-run of data synchronization.""" + + # For sake of a simple example, we don't actually use DiffSync here + try: + with transaction.atomic(): + site, created = Site.objects.get_or_create( + slug=self.data["site_slug"], + defaults={"name": self.data["site_slug"]}, + ) + self.sync_log( + action=SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + changed_object=site, + ) + + if self.dry_run: + raise AbortTransaction() + except AbortTransaction: + self.job_log("Database changes have been reverted automatically.") diff --git a/nautobot_data_sync/tables.py b/nautobot_data_sync/tables.py new file mode 100644 index 000000000..305f2b250 --- /dev/null +++ b/nautobot_data_sync/tables.py @@ -0,0 +1,135 @@ +"""Data tables for data synchronization.""" + +from django_tables2 import Column, JSONColumn, LinkColumn, TemplateColumn + +from nautobot.utilities.tables import BaseTable, ToggleColumn + +from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from .models import Sync, SyncLogEntry + + +ACTION_LOGS_LINK = """ + + {{ value }} + +""" + + +STATUS_LOGS_LINK = """ + + {{ value }} + +""" + + +DRY_RUN_LABEL = """ +{% if record.dry_run %} +Dry Run +{% else %} +Sync +{% endif %} +""" + + +MESSAGE_SPAN = """{% if record.message %}{{ record.message }}{% else %}—{% endif %}""" + + +class SyncTable(BaseTable): + """Table for listing Sync records.""" + + pk = ToggleColumn() + created = LinkColumn(text=lambda sync: sync.job_result.created) + name = Column(accessor="job_result.name") + dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") + + num_unchanged = TemplateColumn( + template_code=ACTION_LOGS_LINK, + verbose_name="No change", + extra_context={"link_class": "num_unchanged", "action": SyncLogEntryActionChoices.ACTION_NO_CHANGE}, + ) + num_created = TemplateColumn( + template_code=ACTION_LOGS_LINK, + verbose_name="Create", + extra_context={"link_class": "num_created", "action": SyncLogEntryActionChoices.ACTION_CREATE}, + ) + num_updated = TemplateColumn( + template_code=ACTION_LOGS_LINK, + verbose_name="Update", + extra_context={"link_class": "num_updated", "action": SyncLogEntryActionChoices.ACTION_UPDATE}, + ) + num_deleted = TemplateColumn( + template_code=ACTION_LOGS_LINK, + verbose_name="Delete", + extra_context={"link_class": "num_deleted", "action": SyncLogEntryActionChoices.ACTION_DELETE}, + ) + + num_succeeded = TemplateColumn( + template_code=STATUS_LOGS_LINK, + verbose_name="Success", + extra_context={"link_class": "num_succeeded", "status": SyncLogEntryStatusChoices.STATUS_SUCCESS}, + ) + num_failed = TemplateColumn( + template_code=STATUS_LOGS_LINK, + verbose_name="Failure", + extra_context={"link_class": "num_failed", "status": SyncLogEntryStatusChoices.STATUS_FAILURE}, + ) + num_errored = TemplateColumn( + template_code=STATUS_LOGS_LINK, + verbose_name="Error", + extra_context={"link_class": "num_errored", "status": SyncLogEntryStatusChoices.STATUS_ERROR}, + ) + + message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) + + class Meta(BaseTable.Meta): + model = Sync + fields = ( + "pk", + "created", + "name", + "user", + "dry_run", + "num_unchanged", + "num_created", + "num_updated", + "num_deleted", + "num_succeeded", + "num_failed", + "num_errored", + "message", + ) + default_columns = ( + "pk", + "created", + "name", + "dry_run", + "num_created", + "num_updated", + "num_deleted", + "num_failed", + "num_errored", + "message", + ) + + +ACTION_LABEL = """{{ record.action }}""" + + +LOG_STATUS_LABEL = """{{ record.status }}""" + + +class SyncLogEntryTable(BaseTable): + """Table for displaying SyncLogEntry records.""" + + pk = ToggleColumn() + sync = Column(accessor="sync__id") + action = TemplateColumn(template_code=ACTION_LABEL) + diff = JSONColumn(orderable=False) + status = TemplateColumn(template_code=LOG_STATUS_LABEL) + message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) + + class Meta(BaseTable.Meta): + model = SyncLogEntry + fields = ("pk", "timestamp", "sync", "action", "changed_object", "diff", "status", "message") diff --git a/nautobot_data_sync/templates/nautobot_data_sync/sync.html b/nautobot_data_sync/templates/nautobot_data_sync/sync.html new file mode 100644 index 000000000..c03318f59 --- /dev/null +++ b/nautobot_data_sync/templates/nautobot_data_sync/sync.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} +{% load helpers %} +{% load form_helpers %} + +{% block title %}{{ sync_worker_class.name }}{% endblock %} + +{% block content %} +
+
+ +
+
+

{{ sync_worker_class.name }}

+

{{ sync_worker_class.description }}

+
+
+
+ {% csrf_token %} +
+
+ Sync Parameters +
+
+ {% render_form form %} +
+
+
+ + Cancel +
+
+
+
+{% endblock %} diff --git a/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html new file mode 100644 index 000000000..2503ba312 --- /dev/null +++ b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} +{% load buttons %} +{% load custom_links %} +{% load plugins %} + +{% block header %} +
+
+ +
+
+
+ {% plugin_buttons object %} + {% if perms.nautobot_data_sync.delete_sync %} + {% delete_button object %} + {% endif %} +
+

{% block title %}{{ object }}{% endblock %}

+ {% include 'inc/created_updated.html' %} +
+ {% custom_links object %} +
+ +{% endblock %} + +{% block content %} +
+
+
+
+ Summary +
+ + + + + +
Dry run?{{ object.dry_run }}
+
+ {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/relationships_panel.html' %} + {% plugin_left_page object %} +
+
+
+
+ Diff +
+
{{ diff }}
+
+ {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/nautobot_data_sync/templates/nautobot_data_sync/sync_home.html b/nautobot_data_sync/templates/nautobot_data_sync/sync_home.html new file mode 100644 index 000000000..c393107d4 --- /dev/null +++ b/nautobot_data_sync/templates/nautobot_data_sync/sync_home.html @@ -0,0 +1,115 @@ +{% extends 'base.html' %} +{% load buttons %} +{% load helpers %} +{% load static %} + +{% block content %} +

{% block title %}Data Synchronization{% endblock %}

+ +

Synchronization Operations

+
+
+
+ + + + + + + + + + {% for sync_worker_class in sync_worker_classes %} + + + + + + {% endfor %} + +
NameDescription
+ + + Execute + + + {{ sync_worker_class.name }} + + {{ sync_worker_class.description }} +
+
+
+
+ +

Previous Data Syncs

+
+ {% block buttons %}{% endblock %} + {% if request.user.is_authenticated and table_config_form %} + + {% endif %} +
+
+
+
+ {% include 'inc/search_panel.html' %} + {% block sidebar %}{% endblock %} +
+ {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %} + {% if permissions.change or permissions.delete %} +
+ {% csrf_token %} + + {% if table.paginator.num_pages > 1 %} + + {% endif %} + {% include table_template|default:'responsive_table.html' %} +
+ {% block bulk_buttons %}{% endblock %} + {% if bulk_edit_url and permissions.change %} + + {% endif %} + {% if bulk_delete_url and permissions.delete %} + + {% endif %} +
+
+ {% else %} + {% include table_template|default:'responsive_table.html' %} + {% endif %} + {% endwith %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} +
+
+
+ {% table_config_form table table_name="ObjectTable" %} +{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/nautobot_data_sync/urls.py b/nautobot_data_sync/urls.py index 00e5486fe..a8c5449d0 100644 --- a/nautobot_data_sync/urls.py +++ b/nautobot_data_sync/urls.py @@ -1,4 +1,17 @@ """Django urlpatterns declaration for nautobot_data_sync plugin.""" -# from django.urls import path -urlpatterns = [] +from django.urls import path + +from nautobot.extras.views import ObjectChangeLogView + +from . import models, views + +urlpatterns = [ + path("syncs/", views.SyncListView.as_view(), name="sync_list"), + path("syncs/start//", views.SyncCreateView.as_view(), name="sync_add"), + path("syncs/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), + path("syncs//", views.SyncView.as_view(), name="sync"), + path("syncs//changelog/", ObjectChangeLogView.as_view(), name="sync_changelog", kwargs={"model": models.Sync}), + path("syncs//delete/", views.SyncDeleteView.as_view(), name="sync_delete"), + path("logs/", views.SyncLogEntryListView.as_view(), name="synclogentry_list"), +] diff --git a/nautobot_data_sync/views.py b/nautobot_data_sync/views.py new file mode 100644 index 000000000..6f2a69401 --- /dev/null +++ b/nautobot_data_sync/views.py @@ -0,0 +1,138 @@ +"""Django views for data synchronization.""" + +import pprint + +from django.contrib.contenttypes.models import ContentType +from django.contrib import messages +from django.db import transaction +from django.http import Http404 +from django.shortcuts import redirect, render + +from django_rq import get_queue +from django_rq.queues import get_connection +from rq import Worker + +from nautobot.extras.models import JobResult +from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectView + +from .filters import SyncFilter, SyncLogEntryFilter +from .forms import SyncFilterForm, SyncLogEntryFilterForm +from .models import Sync, SyncLogEntry +from .sync import get_sync_worker_class, get_sync_worker_classes +from .tables import SyncTable, SyncLogEntryTable + + +class SyncListView(ObjectListView): + """View for listing Sync records.""" + + queryset = Sync.objects.all() + filterset = SyncFilter + filterset_form = SyncFilterForm + table = SyncTable + action_buttons = [] + template_name = "nautobot_data_sync/sync_home.html" + + def extra_context(self): + return {"sync_worker_classes": get_sync_worker_classes()} + + +class SyncCreateView(ObjectEditView): + """View for starting a new Sync.""" + + queryset = Sync.objects.all() + + def get(self, request, sync_worker_slug): + """Render a form for executing the given sync worker.""" + + try: + sync_worker_class = get_sync_worker_class(slug=sync_worker_slug) + except KeyError: + raise Http404 + + form = sync_worker_class.as_form(initial=request.GET) + + return render( + request, + "nautobot_data_sync/sync.html", + { + "sync_worker_class": sync_worker_class, + "form": form, + }, + ) + + def post(self, request, sync_worker_slug): + """Enqueue the given sync worker for execution!""" + try: + sync_worker_class = get_sync_worker_class(slug=sync_worker_slug) + except KeyError: + raise Http404 + + if not Worker.count(get_connection("default")): + messages.error(request, "Unable to perform sync: RQ worker process not running.") + + form = sync_worker_class.as_form(request.POST, request.FILES) + + if form.is_valid(): + dry_run = form.cleaned_data.pop("dry_run") + + sync = Sync.objects.create(dry_run=dry_run, diff={}) + job_result = JobResult.objects.create( + name=sync_worker_class.name, + obj_type=ContentType.objects.get_for_model(sync), + user=request.user, + job_id=sync.pk, + ) + sync.job_result = job_result + sync.save() + + transaction.on_commit( + lambda: get_queue("default").enqueue( + "nautobot_data_sync.sync.sync", sync_id=sync.pk, data=form.cleaned_data, job_timeout=3600 + ) + ) + + return redirect("plugins:nautobot_data_sync:sync", pk=sync.pk) + + return render( + request, + "nautobot_data_sync/sync.html", + { + "sync_worker_class": sync_worker_class, + "form": form, + }, + ) + + +class SyncDeleteView(ObjectDeleteView): + """View for deleting a single Sync record.""" + queryset = Sync.objects.all() + + +class SyncBulkDeleteView(BulkDeleteView): + """View for bulk-deleting Sync records.""" + + queryset = Sync.objects.all() + table = SyncTable + + +class SyncView(ObjectView): + """View for details of a single Sync record.""" + + queryset = Sync.objects.all() + template_name = "nautobot_data_sync/sync_detail.html" + + def get_extra_context(self, request, instance): + """Add additional context to the view.""" + return { + "diff": pprint.pformat(instance.diff, width=180, compact=True), + } + + +class SyncLogEntryListView(ObjectListView): + """View for listing SyncLogEntry records.""" + + queryset = SyncLogEntry.objects.all() + filterset = SyncLogEntryFilter + filterset_form = SyncLogEntryFilterForm + table = SyncLogEntryTable + action_buttons = [] diff --git a/poetry.lock b/poetry.lock index 24da15ba9..e1ac18eb9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -196,9 +196,9 @@ test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pret [[package]] name = "dataclasses" -version = "0.8" +version = "0.7" description = "A backport of the dataclasses module for Python 3.6" -category = "dev" +category = "main" optional = false python-versions = ">=3.6, <3.7" @@ -210,6 +210,20 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "diffsync" +version = "1.3.0" +description = "Library to easily sync/diff/update 2 different data sources" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +colorama = ">=0.4.3,<0.5.0" +dataclasses = {version = ">=0.7,<0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} +pydantic = ">=1.7.2,<2.0.0" +structlog = ">=20.1.0,<21.0.0" + [[package]] name = "django" version = "3.1.11" @@ -974,6 +988,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pydantic" +version = "1.7.4" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] + [[package]] name = "pydocstyle" version = "6.0.0" @@ -1294,6 +1324,22 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[[package]] +name = "structlog" +version = "20.2.0" +description = "Structured Logging for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] +docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] + [[package]] name = "svgwrite" version = "1.4.1" @@ -1424,7 +1470,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "a27e40a2d515ae40fa0c6b0f753487ef5a78f00d4f2da1430fd20486fa40efcd" +content-hash = "69aa4a5b32ba3034d5129c30edd7ef48b5230c33f057a895d6cb57c8e98e71ae" [metadata.files] aniso8601 = [ @@ -1586,13 +1632,17 @@ cryptography = [ {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, ] defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +diffsync = [ + {file = "diffsync-1.3.0-py3-none-any.whl", hash = "sha256:c23bac1080ea7205272bacb4ae4659a23a96172a28024febe162a5559e178d0b"}, + {file = "diffsync-1.3.0.tar.gz", hash = "sha256:ab5499293e307f872056a757bea22e0c88ec91e88dfdba4d6bdc59832835b2af"}, +] django = [ {file = "Django-3.1.11-py3-none-any.whl", hash = "sha256:c79245c488411d1ae300b8f7a08ac18a496380204cf3035aff97ad917a8de999"}, {file = "Django-3.1.11.tar.gz", hash = "sha256:9a0a2f3d34c53032578b54db7ec55929b87dda6fec27a06cc2587afbea1965e5"}, @@ -1971,6 +2021,30 @@ pycryptodome = [ {file = "pycryptodome-3.10.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713"}, {file = "pycryptodome-3.10.1.tar.gz", hash = "sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673"}, ] +pydantic = [ + {file = "pydantic-1.7.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3c60039e84552442defbcb5d56711ef0e057028ca7bfc559374917408a88d84e"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6e7e314acb170e143c6f3912f93f2ec80a96aa2009ee681356b7ce20d57e5c62"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:8ef77cd17b73b5ba46788d040c0e820e49a2d80cfcd66fda3ba8be31094fd146"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:115d8aa6f257a1d469c66b6bfc7aaf04cd87c25095f24542065c68ebcb42fe63"}, + {file = "pydantic-1.7.4-cp36-cp36m-win_amd64.whl", hash = "sha256:66757d4e1eab69a3cfd3114480cc1d72b6dd847c4d30e676ae838c6740fdd146"}, + {file = "pydantic-1.7.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c92863263e4bd89e4f9cf1ab70d918170c51bd96305fe7b00853d80660acb26"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3b8154babf30a5e0fa3aa91f188356763749d9b30f7f211fafb247d4256d7877"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:80cc46378505f7ff202879dcffe4bfbf776c15675028f6e08d1d10bdfbb168ac"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:dda60d7878a5af2d8560c55c7c47a8908344aa78d32ec1c02d742ede09c534df"}, + {file = "pydantic-1.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4c1979d5cc3e14b35f0825caddea5a243dd6085e2a7539c006bc46997ef7a61a"}, + {file = "pydantic-1.7.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8857576600c32aa488f18d30833aa833b54a48e3bab3adb6de97e463af71f8f8"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f86d4da363badb39426a0ff494bf1d8510cd2f7274f460eee37bdbf2fd495ec"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3ea1256a9e782149381e8200119f3e2edea7cd6b123f1c79ab4bbefe4d9ba2c9"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e28455b42a0465a7bf2cde5eab530389226ce7dc779de28d17b8377245982b1e"}, + {file = "pydantic-1.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:47c5b1d44934375a3311891cabd450c150a31cf5c22e84aa172967bf186718be"}, + {file = "pydantic-1.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00250e5123dd0b123ff72be0e1b69140e0b0b9e404d15be3846b77c6f1b1e387"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d24aa3f7f791a023888976b600f2f389d3713e4f23b7a4c88217d3fce61cdffc"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2c44a9afd4c4c850885436a4209376857989aaf0853c7b118bb2e628d4b78c4e"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:e87edd753da0ca1d44e308a1b1034859ffeab1f4a4492276bff9e1c3230db4fe"}, + {file = "pydantic-1.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:a3026ee105b5360855e500b4abf1a1d0b034d88e75a2d0d66a4c35e60858e15b"}, + {file = "pydantic-1.7.4-py3-none-any.whl", hash = "sha256:a82385c6d5a77e3387e94612e3e34b77e13c39ff1295c26e3ba664e7b98073e2"}, + {file = "pydantic-1.7.4.tar.gz", hash = "sha256:0a1abcbd525fbb52da58c813d54c2ec706c31a91afdb75411a73dd1dec036595"}, +] pydocstyle = [ {file = "pydocstyle-6.0.0-py3-none-any.whl", hash = "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"}, {file = "pydocstyle-6.0.0.tar.gz", hash = "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f"}, @@ -2174,6 +2248,10 @@ stevedore = [ {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, ] +structlog = [ + {file = "structlog-20.2.0-py2.py3-none-any.whl", hash = "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd"}, + {file = "structlog-20.2.0.tar.gz", hash = "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174"}, +] svgwrite = [ {file = "svgwrite-1.4.1-py3-none-any.whl", hash = "sha256:4b21652a1d9c543a6bf4f9f2a54146b214519b7540ca60cb99968ad09ef631d0"}, {file = "svgwrite-1.4.1.zip", hash = "sha256:e220a4bf189e7e214a55e8a11421d152b5b6fb1dd660c86a8b6b61fe8cc2ac48"}, diff --git a/pyproject.toml b/pyproject.toml index 22e9cb28c..db53400bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.6" nautobot = {git = "https://github.com/nautobot/nautobot", rev = "develop"} +diffsync = "^1.3.0" [tool.poetry.dev-dependencies] invoke = "*" @@ -38,6 +39,9 @@ coverage = "*" mkdocs = "*" markdown-include = "*" +[tool.poetry.plugins."nautobot_data_sync.sync_worker_classes"] +"example" = "nautobot_data_sync.sync.example:ExampleSyncWorker" + [tool.black] line-length = 120 target-version = ['py37'] From 04c867c72e402211682a5db48c8bc19d084b44a9 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 24 May 2021 17:20:39 -0400 Subject: [PATCH 02/42] More progress towards a basic functioning UI --- development/nautobot_config.py | 39 +++++++- nautobot_data_sync/migrations/0001_initial.py | 89 +++++++++++++------ nautobot_data_sync/models.py | 31 ++++++- nautobot_data_sync/sync/__init__.py | 26 ++++-- nautobot_data_sync/sync/base.py | 2 + nautobot_data_sync/sync/example.py | 17 ++-- nautobot_data_sync/tables.py | 16 ++-- .../nautobot_data_sync/sync_detail.html | 5 ++ nautobot_data_sync/urls.py | 7 +- nautobot_data_sync/views.py | 3 +- poetry.lock | 8 +- pyproject.toml | 2 +- 12 files changed, 189 insertions(+), 56 deletions(-) diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 3eaf7c1a0..05e3eefb2 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -202,7 +202,44 @@ def is_truthy(arg): # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: # https://docs.djangoproject.com/en/stable/topics/logging/ -LOGGING = {} +LOG_LEVEL = "DEBUG" if DEBUG else "INFO" +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "normal": { + "format": "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)s :\n %(message)s", + "datefmt": "%H:%M:%S", + }, + "verbose": { + "format": "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-20s %(filename)-15s %(funcName)30s() :\n %(message)s", + "datefmt": "%H:%M:%S", + }, + }, + "handlers": { + "normal_console": { + "level": "INFO", + "class": "rq.utils.ColorizingStreamHandler", + "formatter": "normal", + }, + "verbose_console": { + "level": "DEBUG", + "class": "rq.utils.ColorizingStreamHandler", + "formatter": "verbose", + }, + }, + "loggers": { + "django": {"handlers": ["normal_console"], "level": "INFO"}, + "nautobot": { + "handlers": ["verbose_console" if DEBUG else "normal_console"], + "level": LOG_LEVEL, + }, + "rq.worker": { + "handlers": ["verbose_console" if DEBUG else "normal_console"], + "level": LOG_LEVEL, + }, + }, +} # Setting this to True will display a "maintenance mode" banner at the top of every page. MAINTENANCE_MODE = False diff --git a/nautobot_data_sync/migrations/0001_initial.py b/nautobot_data_sync/migrations/0001_initial.py index 577e105a3..90972d611 100644 --- a/nautobot_data_sync/migrations/0001_initial.py +++ b/nautobot_data_sync/migrations/0001_initial.py @@ -11,44 +11,83 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('extras', '0005_configcontext_device_types'), - ('contenttypes', '0002_remove_content_type_name'), + ("extras", "0005_configcontext_device_types"), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( - name='Sync', + name="Sync", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('_custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), - ('dry_run', models.BooleanField(default=False)), - ('diff', models.JSONField()), - ('job_result', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='extras.jobresult')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("created", models.DateField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), + ( + "_custom_field_data", + models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + ("dry_run", models.BooleanField(default=False)), + ("diff", models.JSONField()), + ( + "job_result", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="extras.jobresult" + ), + ), ], options={ - 'ordering': ['-created'], + "ordering": ["-created"], }, ), migrations.CreateModel( - name='SyncLogEntry', + name="SyncLogEntry", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('action', models.CharField(max_length=32)), - ('status', models.CharField(max_length=32)), - ('diff', models.JSONField()), - ('changed_object_id', models.UUIDField(blank=True, null=True)), - ('object_repr', models.CharField(editable=False, max_length=200)), - ('message', models.CharField(blank=True, max_length=511)), - ('changed_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')), - ('object_change', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='extras.objectchange')), - ('sync', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', related_query_name='log', to='nautobot_data_sync.sync')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ("action", models.CharField(max_length=32)), + ("status", models.CharField(max_length=32)), + ("diff", models.JSONField()), + ("changed_object_id", models.UUIDField(blank=True, null=True)), + ("object_repr", models.CharField(editable=False, max_length=200)), + ("message", models.CharField(blank=True, max_length=511)), + ( + "changed_object_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="contenttypes.contenttype", + ), + ), + ( + "object_change", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="extras.objectchange" + ), + ), + ( + "sync", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logs", + related_query_name="log", + to="nautobot_data_sync.sync", + ), + ), ], options={ - 'verbose_name_plural': 'sync log entries', - 'ordering': ['sync', 'timestamp'], + "verbose_name_plural": "sync log entries", + "ordering": ["sync", "timestamp"], }, ), ] diff --git a/nautobot_data_sync/models.py b/nautobot_data_sync/models.py index 9a4b4d097..7bb3ad082 100644 --- a/nautobot_data_sync/models.py +++ b/nautobot_data_sync/models.py @@ -46,6 +46,28 @@ class Meta: def get_absolute_url(self): return reverse("plugins:nautobot_data_sync:sync", kwargs={"pk": self.pk}) + @classmethod + def queryset(cls): + """Construct an efficient queryset for this model and related data.""" + return ( + cls.objects.defer("diff") + .select_related("job_result") + .prefetch_related("logs") + .annotate( + num_unchanged=models.Count( + "log", filter=models.Q(log__action=SyncLogEntryActionChoices.ACTION_NO_CHANGE) + ), + num_created=models.Count("log", filter=models.Q(log__action=SyncLogEntryActionChoices.ACTION_CREATE)), + num_updated=models.Count("log", filter=models.Q(log__action=SyncLogEntryActionChoices.ACTION_UPDATE)), + num_deleted=models.Count("log", filter=models.Q(log__action=SyncLogEntryActionChoices.ACTION_DELETE)), + num_succeeded=models.Count( + "log", filter=models.Q(log__status=SyncLogEntryStatusChoices.STATUS_SUCCESS) + ), + num_failed=models.Count("log", filter=models.Q(log__status=SyncLogEntryStatusChoices.STATUS_FAILURE)), + num_errored=models.Count("log", filter=models.Q(log__status=SyncLogEntryStatusChoices.STATUS_ERROR)), + ) + ) + class SyncLogEntry(BaseModel): """Record of a single event during a data sync operation. @@ -59,9 +81,7 @@ class SyncLogEntry(BaseModel): may reflect this or may reflect a change that *could not happen* or *failed*. """ - sync = models.ForeignKey( - to=Sync, on_delete=models.CASCADE, related_name="logs", related_query_name="log" - ) + sync = models.ForeignKey(to=Sync, on_delete=models.CASCADE, related_name="logs", related_query_name="log") timestamp = models.DateTimeField(auto_now_add=True) action = models.CharField(max_length=32, choices=SyncLogEntryActionChoices) @@ -69,7 +89,10 @@ class SyncLogEntry(BaseModel): diff = models.JSONField() changed_object_type = models.ForeignKey( - to=ContentType, blank=True, null=True, on_delete=models.PROTECT, + to=ContentType, + blank=True, + null=True, + on_delete=models.PROTECT, ) changed_object_id = models.UUIDField(blank=True, null=True) changed_object = GenericForeignKey(ct_field="changed_object_type", fk_field="changed_object_id") diff --git a/nautobot_data_sync/sync/__init__.py b/nautobot_data_sync/sync/__init__.py index 0d6c9a86e..7a230c81d 100644 --- a/nautobot_data_sync/sync/__init__.py +++ b/nautobot_data_sync/sync/__init__.py @@ -9,7 +9,7 @@ from django_rq import job import structlog -from nautobot.extras.choices import JobResultStatusChoices +from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices from nautobot_data_sync.choices import SyncLogEntryActionChoices from nautobot_data_sync.models import Sync, SyncLogEntry @@ -65,14 +65,18 @@ def sync(sync_id, data): """Perform a requested sync.""" sync = Sync.objects.get(id=sync_id) - logger.info("START: data synchronization %s", sync) - + sync.job_result.log( + f"START: data synchronization {sync}", grouping="sync", level_choice=LogLevelChoices.LOG_INFO, logger=logger + ) sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) sync.job_result.save() try: structlog.configure( - processors=[log_to_log_entry, structlog.stdlib.render_to_log_kwargs,], + processors=[ + log_to_log_entry, + structlog.stdlib.render_to_log_kwargs, + ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, @@ -84,10 +88,20 @@ def sync(sync_id, data): sync_worker.execute(dry_run=sync.dry_run) except Exception as exc: - logger.error("Exception occurred during %s: %s", sync, exc) + sync.job_result.log( + f"Exception occurred during {sync}: {exc}", + grouping="sync", + level_choice=LogLevelChoices.LOG_FAILURE, + logger=logger, + ) sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) else: - logger.info("FINISH: data synchronization %s", sync) + sync.job_result.log( + f"FINISH: data synchronization {sync}", + grouping="sync", + level_choice=LogLevelChoices.LOG_INFO, + logger=logger, + ) sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) finally: sync.job_result.completed = timezone.now() diff --git a/nautobot_data_sync/sync/base.py b/nautobot_data_sync/sync/base.py index f9b7eaea6..10662cea6 100644 --- a/nautobot_data_sync/sync/base.py +++ b/nautobot_data_sync/sync/base.py @@ -33,6 +33,7 @@ class Meta: - slug (cls.__name__) - URL-safe unique identifier of this worker class. - description ("")- Detailed description of this worker class. """ + dry_run_default = True @classproperty @@ -97,6 +98,7 @@ def sync_log( self, action, status, + message="", diff=None, changed_object=None, object_repr=None, diff --git a/nautobot_data_sync/sync/example.py b/nautobot_data_sync/sync/example.py index 8461d5152..135118cbf 100644 --- a/nautobot_data_sync/sync/example.py +++ b/nautobot_data_sync/sync/example.py @@ -9,6 +9,7 @@ from nautobot_data_sync.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices from nautobot_data_sync.sync.base import DataSyncWorker + class ExampleSyncWorker(DataSyncWorker): site_slug = StringVar(description="Which site's data to synchronize", default="") @@ -19,7 +20,7 @@ class Meta: description = "An example of how a sync worker might be implemented" def execute(self, dry_run=True): - """Perform a dry-run of data synchronization.""" + """Perform a mock data synchronization.""" # For sake of a simple example, we don't actually use DiffSync here try: @@ -28,13 +29,13 @@ def execute(self, dry_run=True): slug=self.data["site_slug"], defaults={"name": self.data["site_slug"]}, ) - self.sync_log( - action=SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE, - status=SyncLogEntryStatusChoices.STATUS_SUCCESS, - changed_object=site, - ) - - if self.dry_run: + if dry_run: raise AbortTransaction() except AbortTransaction: self.job_log("Database changes have been reverted automatically.") + + self.sync_log( + action=SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + changed_object=site, + ) diff --git a/nautobot_data_sync/tables.py b/nautobot_data_sync/tables.py index 305f2b250..f7d350c1a 100644 --- a/nautobot_data_sync/tables.py +++ b/nautobot_data_sync/tables.py @@ -1,6 +1,6 @@ """Data tables for data synchronization.""" -from django_tables2 import Column, JSONColumn, LinkColumn, TemplateColumn +from django_tables2 import Column, DateTimeColumn, JSONColumn, LinkColumn, TemplateColumn from nautobot.utilities.tables import BaseTable, ToggleColumn @@ -40,9 +40,10 @@ class SyncTable(BaseTable): """Table for listing Sync records.""" pk = ToggleColumn() - created = LinkColumn(text=lambda sync: sync.job_result.created) + timestamp = DateTimeColumn(accessor="job_result.created", linkify=True, short=True, verbose_name="Timestamp") name = Column(accessor="job_result.name") dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") + status = TemplateColumn(template_code="{% include 'extras/inc/job_label.html' with result=record.job_result %}") num_unchanged = TemplateColumn( template_code=ACTION_LOGS_LINK, @@ -87,9 +88,10 @@ class Meta(BaseTable.Meta): model = Sync fields = ( "pk", - "created", + "timestamp", "name", "user", + "status", "dry_run", "num_unchanged", "num_created", @@ -102,8 +104,9 @@ class Meta(BaseTable.Meta): ) default_columns = ( "pk", - "created", + "timestamp", "name", + "status", "dry_run", "num_created", "num_updated", @@ -112,6 +115,7 @@ class Meta(BaseTable.Meta): "num_errored", "message", ) + order_by = ("-timestamp",) ACTION_LABEL = """{{ record.action }}""" @@ -124,12 +128,14 @@ class SyncLogEntryTable(BaseTable): """Table for displaying SyncLogEntry records.""" pk = ToggleColumn() - sync = Column(accessor="sync__id") + sync = LinkColumn(accessor="sync__id", verbose_name="Sync") action = TemplateColumn(template_code=ACTION_LABEL) diff = JSONColumn(orderable=False) status = TemplateColumn(template_code=LOG_STATUS_LABEL) message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) + changed_object = LinkColumn(verbose_name="Changed object") class Meta(BaseTable.Meta): model = SyncLogEntry fields = ("pk", "timestamp", "sync", "action", "changed_object", "diff", "status", "message") + order_by = ("-timestamp",) diff --git a/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html index 2503ba312..443a15504 100644 --- a/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html +++ b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html @@ -49,6 +49,10 @@

{% block title %}{{ object }}{% endblock %}

Dry run? {{ object.dry_run }} + + Job result + {{ object.job_result }} + {% include 'inc/custom_fields_panel.html' %} @@ -67,6 +71,7 @@

{% block title %}{{ object }}{% endblock %}

+ {% include "extras/inc/jobresult.html" with result=object.job_result %} {% plugin_full_width_page object %}
diff --git a/nautobot_data_sync/urls.py b/nautobot_data_sync/urls.py index a8c5449d0..f7797571c 100644 --- a/nautobot_data_sync/urls.py +++ b/nautobot_data_sync/urls.py @@ -11,7 +11,12 @@ path("syncs/start//", views.SyncCreateView.as_view(), name="sync_add"), path("syncs/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), path("syncs//", views.SyncView.as_view(), name="sync"), - path("syncs//changelog/", ObjectChangeLogView.as_view(), name="sync_changelog", kwargs={"model": models.Sync}), + path( + "syncs//changelog/", + ObjectChangeLogView.as_view(), + name="sync_changelog", + kwargs={"model": models.Sync}, + ), path("syncs//delete/", views.SyncDeleteView.as_view(), name="sync_delete"), path("logs/", views.SyncLogEntryListView.as_view(), name="synclogentry_list"), ] diff --git a/nautobot_data_sync/views.py b/nautobot_data_sync/views.py index 6f2a69401..f326eb080 100644 --- a/nautobot_data_sync/views.py +++ b/nautobot_data_sync/views.py @@ -25,7 +25,7 @@ class SyncListView(ObjectListView): """View for listing Sync records.""" - queryset = Sync.objects.all() + queryset = Sync.queryset() filterset = SyncFilter filterset_form = SyncFilterForm table = SyncTable @@ -105,6 +105,7 @@ def post(self, request, sync_worker_slug): class SyncDeleteView(ObjectDeleteView): """View for deleting a single Sync record.""" + queryset = Sync.objects.all() diff --git a/poetry.lock b/poetry.lock index e1ac18eb9..cbd4ed1a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -846,9 +846,9 @@ svgwrite = "~1.4.1" [package.source] type = "git" -url = "https://github.com/nautobot/nautobot" -reference = "develop" -resolved_reference = "386ecaa57bf3c49d4427d9151df2e2ebbc68d7d1" +url = "https://github.com/nautobot/nautobot.git" +reference = "gfm-jobresult-view" +resolved_reference = "bdb91f11b3cbc3948592b1aedb050c34df26c3d2" [[package]] name = "netaddr" @@ -1470,7 +1470,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "69aa4a5b32ba3034d5129c30edd7ef48b5230c33f057a895d6cb57c8e98e71ae" +content-hash = "43b4622b29e6e6d18c93c5673063f62e4f71236033788e78baeeaa457d6db479" [metadata.files] aniso8601 = [ diff --git a/pyproject.toml b/pyproject.toml index db53400bd..6b6ffab00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.6" -nautobot = {git = "https://github.com/nautobot/nautobot", rev = "develop"} +nautobot = {git = "https://github.com/nautobot/nautobot.git", rev = "gfm-jobresult-view"} diffsync = "^1.3.0" [tool.poetry.dev-dependencies] From 8899de3626b2804775cff76c3d98f8a1173de4ee Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 28 May 2021 13:47:52 -0400 Subject: [PATCH 03/42] Create nautobot_data_sync_servicenow --- development/Dockerfile | 12 + development/nautobot_config.py | 8 + .../.bandit.yml | 5 + .../.dockerignore | 20 + nautobot-plugin-data-sync-servicenow/.flake8 | 4 + .../.github/CODEOWNERS | 2 + .../.github/ISSUE_TEMPLATE/bug_report.md | 25 + .../.github/ISSUE_TEMPLATE/feature_request.md | 22 + .../pull_request_template.md | 10 + .../.gitignore | 302 ++++ .../.pydocstyle.ini | 11 + .../.travis.yml | 48 + .../.yamllint.yml | 10 + nautobot-plugin-data-sync-servicenow/FAQ.md | 1 + nautobot-plugin-data-sync-servicenow/LICENSE | 15 + .../README.md | 169 ++ .../development/Dockerfile | 19 + .../development/creds.example.env | 10 + .../development/dev.env | 17 + .../development/docker-compose.base.yml | 32 + .../development/docker-compose.dev.yml | 16 + .../development/docker-compose.docs.yml | 11 + .../docker-compose.requirements.yml | 25 + .../development/nautobot_config.py | 320 ++++ .../docs/extra.css | 19 + .../docs/index.md | 17 + .../docs/requirements.txt | 2 + .../invoke.example.yml | 11 + .../mkdocs.yml | 25 + .../nautobot_data_sync_servicenow/__init__.py | 24 + .../api/__init__.py | 1 + .../data/mappings.yaml | 59 + .../diffsync/__init__.py | 0 .../diffsync/adapter_nautobot.py | 86 ++ .../diffsync/adapter_servicenow.py | 146 ++ .../diffsync/models.py | 198 +++ .../migrations/__init__.py | 0 .../servicenow.py | 165 ++ .../tests/__init__.py | 1 + .../tests/test_api.py | 28 + .../tests/test_basic.py | 16 + .../nautobot_data_sync_servicenow/urls.py | 4 + .../utilities.py | 93 ++ .../nautobot_data_sync_servicenow/worker.py | 66 + .../poetry.lock | 1362 +++++++++++++++++ .../pyproject.toml | 102 ++ nautobot-plugin-data-sync-servicenow/tasks.py | 373 +++++ nautobot_data_sync/sync/__init__.py | 2 +- nautobot_data_sync/sync/base.py | 9 +- .../nautobot_data_sync/sync_detail.html | 4 + 50 files changed, 3924 insertions(+), 3 deletions(-) create mode 100644 nautobot-plugin-data-sync-servicenow/.bandit.yml create mode 100644 nautobot-plugin-data-sync-servicenow/.dockerignore create mode 100644 nautobot-plugin-data-sync-servicenow/.flake8 create mode 100644 nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS create mode 100644 nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 nautobot-plugin-data-sync-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 nautobot-plugin-data-sync-servicenow/.gitignore create mode 100644 nautobot-plugin-data-sync-servicenow/.pydocstyle.ini create mode 100644 nautobot-plugin-data-sync-servicenow/.travis.yml create mode 100644 nautobot-plugin-data-sync-servicenow/.yamllint.yml create mode 100644 nautobot-plugin-data-sync-servicenow/FAQ.md create mode 100644 nautobot-plugin-data-sync-servicenow/LICENSE create mode 100644 nautobot-plugin-data-sync-servicenow/README.md create mode 100644 nautobot-plugin-data-sync-servicenow/development/Dockerfile create mode 100644 nautobot-plugin-data-sync-servicenow/development/creds.example.env create mode 100644 nautobot-plugin-data-sync-servicenow/development/dev.env create mode 100644 nautobot-plugin-data-sync-servicenow/development/docker-compose.base.yml create mode 100644 nautobot-plugin-data-sync-servicenow/development/docker-compose.dev.yml create mode 100644 nautobot-plugin-data-sync-servicenow/development/docker-compose.docs.yml create mode 100644 nautobot-plugin-data-sync-servicenow/development/docker-compose.requirements.yml create mode 100644 nautobot-plugin-data-sync-servicenow/development/nautobot_config.py create mode 100644 nautobot-plugin-data-sync-servicenow/docs/extra.css create mode 100644 nautobot-plugin-data-sync-servicenow/docs/index.md create mode 100644 nautobot-plugin-data-sync-servicenow/docs/requirements.txt create mode 100644 nautobot-plugin-data-sync-servicenow/invoke.example.yml create mode 100644 nautobot-plugin-data-sync-servicenow/mkdocs.yml create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/data/mappings.yaml create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/__init__.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_nautobot.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_servicenow.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/models.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/migrations/__init__.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/servicenow.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_api.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_basic.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/utilities.py create mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/worker.py create mode 100644 nautobot-plugin-data-sync-servicenow/poetry.lock create mode 100644 nautobot-plugin-data-sync-servicenow/pyproject.toml create mode 100644 nautobot-plugin-data-sync-servicenow/tasks.py diff --git a/development/Dockerfile b/development/Dockerfile index c9d563798..892b294ef 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -12,8 +12,20 @@ COPY poetry.lock pyproject.toml /source/ # and the project is copied in and installed after this step RUN poetry install --no-interaction --no-ansi --no-root +COPY nautobot-plugin-data-sync-servicenow/poetry.lock nautobot-plugin-data-sync-servicenow/pyproject.toml /source/nautobot-plugin-data-sync-servicenow/ +WORKDIR /source/nautobot-plugin-data-sync-servicenow +RUN poetry install --no-interaction --no-ansi --no-root + # Copy in the rest of the source code and install local Nautobot plugin +WORKDIR /source COPY . /source RUN poetry install --no-interaction --no-ansi +WORKDIR /source/nautobot-plugin-data-sync-servicenow +RUN poetry install --no-interaction --no-ansi + +# Work around https://github.com/python-poetry/poetry/issues/3139 +RUN pip install colorama + +WORKDIR /source COPY development/nautobot_config.py /opt/nautobot/nautobot_config.py diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 05e3eefb2..0d2fe0d23 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -238,6 +238,14 @@ def is_truthy(arg): "handlers": ["verbose_console" if DEBUG else "normal_console"], "level": LOG_LEVEL, }, + "nautobot_data_sync": { + "handlers": ["verbose_console" if DEBUG else "normal_console"], + "level": LOG_LEVEL, + }, + "nautobot_data_sync_servicenow": { + "handlers": ["verbose_console" if DEBUG else "normal_console"], + "level": LOG_LEVEL, + }, }, } diff --git a/nautobot-plugin-data-sync-servicenow/.bandit.yml b/nautobot-plugin-data-sync-servicenow/.bandit.yml new file mode 100644 index 000000000..55c6741b1 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.bandit.yml @@ -0,0 +1,5 @@ +--- +skips: [] +# No need to check for security issues in the test scripts! +exclude_dirs: + - "./tests/" diff --git a/nautobot-plugin-data-sync-servicenow/.dockerignore b/nautobot-plugin-data-sync-servicenow/.dockerignore new file mode 100644 index 000000000..ce354892c --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.dockerignore @@ -0,0 +1,20 @@ +# Docker related +development/Dockerfile +development/docker-compose*.yml +development/*.env +*.env + +# Python +**/*.pyc +**/*.pyo + + +# Other +docs/_build +FAQ.md +.git/ +.gitignore +.github +tasks.py +LICENSE +**/*.log diff --git a/nautobot-plugin-data-sync-servicenow/.flake8 b/nautobot-plugin-data-sync-servicenow/.flake8 new file mode 100644 index 000000000..e3ba27d5d --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.flake8 @@ -0,0 +1,4 @@ +[flake8] +# E501: Line length is enforced by Black, so flake8 doesn't need to check it +# W503: Black disagrees with this rule, as does PEP 8; Black wins +ignore = E501, W503 diff --git a/nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS b/nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS new file mode 100644 index 000000000..42b285c72 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner(s) of all files in this repository +* @glennmatthews @jathanism @lampwins diff --git a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md b/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..9e9a2b088 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: 🐛 Bug Report +about: Report a reproducible bug in the current release of nautobot-data-sync-servicenow +--- + +### Environment +* Python version: +* Nautobot version: +* nautobot-data-sync-servicenow version: + + +### Expected Behavior + + + +### Observed Behavior + + +### Steps to Reproduce +1. +2. +3. diff --git a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md b/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..f2c12b1f4 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: ✨ Feature Request +about: Propose a new feature or enhancement + +--- + +### Environment +* Nautobot version: +* nautobot-data-sync-servicenow version: + + +### Proposed Functionality + + +### Use Case + diff --git a/nautobot-plugin-data-sync-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/nautobot-plugin-data-sync-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..2cf476470 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,10 @@ +## New Pull Request + +Have you: +- [ ] Updated the README if necessary? +- [ ] Updated any configuration settings? +- [ ] Written a unit test? + +## Change Notes + +## Justification diff --git a/nautobot-plugin-data-sync-servicenow/.gitignore b/nautobot-plugin-data-sync-servicenow/.gitignore new file mode 100644 index 000000000..c8dc4550a --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.gitignore @@ -0,0 +1,302 @@ +# Ansible Retry Files +*.retry + +# Swap files +*.swp + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Editor +.vscode/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### vscode ### +.vscode/* +*.code-workspace + +# Rando +creds.env + +# Invoke overrides +invoke.yml diff --git a/nautobot-plugin-data-sync-servicenow/.pydocstyle.ini b/nautobot-plugin-data-sync-servicenow/.pydocstyle.ini new file mode 100644 index 000000000..541cc5106 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.pydocstyle.ini @@ -0,0 +1,11 @@ +[pydocstyle] +convention = google +inherit = false +match = (?!__init__).*\.py +match-dir = (?!tests|migrations|development)[^\.].* +# D212 is enabled by default in google convention, and complains if we have a docstring like: +# """ +# My docstring is on the line after the opening quotes instead of on the same line as them. +# """ +# We've discussed and concluded that we consider this to be a valid style choice. +add_ignore = D212 \ No newline at end of file diff --git a/nautobot-plugin-data-sync-servicenow/.travis.yml b/nautobot-plugin-data-sync-servicenow/.travis.yml new file mode 100644 index 000000000..0f6c4c39c --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.travis.yml @@ -0,0 +1,48 @@ +--- +language: "python" +python: + - "3.6" + - "3.7" + - "3.8" +env: + # Each version of Nautobot listed here must have a corresponding directory/configuration file + # under development/nautobot_/configuration.py + matrix: + - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_NAUTOBOT_VER=1.0.0" +# Add your encrypted secret below, you can encrypt secret using "travis encrypt" +# https://docs.travis-ci.com/user/environment-variables/#defining-encrypted-variables-in-travisyml +# global: +# secure: +services: + - "docker" +# -------------------------------------------------------------------------- +# Tests +# -------------------------------------------------------------------------- +before_script: + - "pip install invoke docker-compose" + - "curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > /tmp/get-poetry.py" + - "python /tmp/get-poetry.py -y --version 1.1.6" + - "source $HOME/.poetry/env" + - "cp development/creds.example.env development/creds.env" + +script: + # If you want to test different versions, it may require updating the poetry definition for Nautobot + # as that is where the version is controlled + # - "poetry add nautobot=$NAUTOBOT_VER" + - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke build --no-cache" + - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke tests --failfast" +# -------------------------------------------------------------------------- +# Deploy +# Uncomment the section below if you would like to publish a new release to pypi automatically +# when a new tag is created in master. You"ll also need to generate a dedicated API key for this project in pypi +# and encrypt the key with "travis encrypt PYPI_TOKEN= --add env.global --com" +# -------------------------------------------------------------------------- +# deploy: +# provider: script +# script: poetry config pypi-token.pypi $PYPI_TOKEN && poetry publish --build +# skip_cleanup: true +# on: +# tags: true +# branch: master +# condition: $NAUTOBOT_VER = master +# python: 3.7 diff --git a/nautobot-plugin-data-sync-servicenow/.yamllint.yml b/nautobot-plugin-data-sync-servicenow/.yamllint.yml new file mode 100644 index 000000000..58324ed12 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/.yamllint.yml @@ -0,0 +1,10 @@ +--- +extends: "default" +rules: + comments: "enable" + empty-values: "enable" + indentation: + indent-sequences: "consistent" + line-length: "disable" + quoted-strings: + quote-type: "double" diff --git a/nautobot-plugin-data-sync-servicenow/FAQ.md b/nautobot-plugin-data-sync-servicenow/FAQ.md new file mode 100644 index 000000000..318b08dc2 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/FAQ.md @@ -0,0 +1 @@ +# Frequently Asked Questions diff --git a/nautobot-plugin-data-sync-servicenow/LICENSE b/nautobot-plugin-data-sync-servicenow/LICENSE new file mode 100644 index 000000000..087f92f5c --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/LICENSE @@ -0,0 +1,15 @@ +Apache Software License 2.0 + +Copyright (c) 2021, Network to Code, LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/nautobot-plugin-data-sync-servicenow/README.md b/nautobot-plugin-data-sync-servicenow/README.md new file mode 100644 index 000000000..6821337f2 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/README.md @@ -0,0 +1,169 @@ +# Nautobot ServiceNow Data Synchronization + +A plugin for [Nautobot](https://github.com/nautobot/nautobot). + +## Installation + +The plugin is available as a Python package in pypi and can be installed with pip + +```shell +pip install nautobot-data-sync-servicenow +``` + +> The plugin is compatible with Nautobot 1.0.0 and higher + +To ensure Nautobot ServiceNow Data Synchronization is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-data-sync-servicenow` package: + +```no-highlight +# echo nautobot-data-sync-servicenow >> local_requirements.txt +``` + +Once installed, the plugin needs to be enabled in your `nautobot_configuration.py` + +```python +# In your configuration.py +PLUGINS = ["nautobot_data_sync_servicenow"] + +# PLUGINS_CONFIG = { +# "nautobot_data_sync_servicenow": { +# ADD YOUR SETTINGS HERE +# } +# } +``` + +The plugin behavior can be controlled with the following list of settings + +- TODO + +## Usage + +### API + +TODO + +## Contributing + +Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of Nautobot through TravisCI. + +The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI. + +The project is following Network to Code software development guideline and is leveraging: + +- Black, Pylint, Bandit and pydocstyle for Python linting and formatting. +- Django unit test to ensure the plugin is working properly. + +### Development Environment + +The development environment can be used in 2 ways. First, with a local poetry environment if you wish to develop outside of Docker. Second, inside of a docker container. + +#### Invoke tasks + +The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: + +* `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest) +* `project_name`: the default docker compose project name (default: nautobot-data-sync-servicenow) +* `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) +* `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) +* `compose_dir`: the full path to a directory containing the project compose files +* `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) + +Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. + +#### Local Poetry Development Environment + +1. Copy `development/creds.example.env` to `development/creds.env` (This file will be ignored by git and docker) +2. Uncomment the `POSTGRES_HOST`, `REDIS_HOST`, and `NAUTOBOT_ROOT` variables in `development/creds.env` +3. Create an invoke.yml with the following contents at the root of the repo: + +```shell +--- +nautobot_data_sync_servicenow: + local: true + compose_files: + - "docker-compose.requirements.yml" +``` + +3. Run the following commands: + +```shell +poetry shell +poetry install +export $(cat development/dev.env | xargs) +export $(cat development/creds.env | xargs) +``` + +4. You can now run nautobot-server commands as you would from the [Nautobot documentation](https://nautobot.readthedocs.io/en/latest/) for example to start the development server: + +```shell +nautobot-server runserver 0.0.0.0:8080 --insecure +``` + +Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). + +#### Docker Development Environment + +This project is managed by [Python Poetry](https://python-poetry.org/) and has a few requirements to setup your development environment: + +1. Install Poetry, see the [Poetry Documentation](https://python-poetry.org/docs/#installation) for your operating system. +2. Install Docker, see the [Docker documentation](https://docs.docker.com/get-docker/) for your operating system. + +Once you have Poetry and Docker installed you can run the following commands to install all other development dependencies in an isolated python virtual environment: + +```shell +poetry shell +poetry install +invoke start +``` + +Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). + +### CLI Helper Commands + +The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. + +Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_PYTHON_VER` and `INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` + +#### Docker dev environment + +```no-highlight + build Build all docker images. + debug Start Nautobot and its dependencies in debug mode. + destroy Destroy all containers and volumes. + restart Restart Nautobot and its dependencies. + start Start Nautobot and its dependencies in detached mode. + stop Stop Nautobot and its dependencies. +``` + +#### Utility + +```no-highlight + cli Launch a bash shell inside the running Nautobot container. + create-user Create a new user in django (default: admin), will prompt for password. + makemigrations Run Make Migration in Django. + nbshell Launch a nbshell session. +``` + +#### Testing + +```no-highlight + bandit Run bandit to validate basic static code security analysis. + black Run black to check that Python files adhere to its style standards. + flake8 This will run flake8 for the specified name and Python version. + pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. + pylint Run pylint code analysis. + tests Run all tests for this plugin. + unittest Run Django unit tests for the plugin. +``` + +### Project Documentation + +Project documentation is generated by [mkdocs](https://www.mkdocs.org/) from the documentation located in the docs folder. You can configure [readthedocs.io](https://readthedocs.io/) to point at this folder in your repo. For development purposes a `docker-compose.docs.yml` is also included. A container hosting the docs will be started using the invoke commands on [http://localhost:8001](http://localhost:8001), as changes are saved the docs will be automatically reloaded. + +## Questions + +For any questions or comments, please check the [FAQ](FAQ.md) first and feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode). +Sign up [here](http://slack.networktocode.com/) + +## Screenshots + +TODO diff --git a/nautobot-plugin-data-sync-servicenow/development/Dockerfile b/nautobot-plugin-data-sync-servicenow/development/Dockerfile new file mode 100644 index 000000000..c9d563798 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/Dockerfile @@ -0,0 +1,19 @@ + +ARG PYTHON_VER +ARG NAUTOBOT_VER +# FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} +FROM nniehoff/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} + +WORKDIR /source + +# Copy in only pyproject.toml/poetry.lock to help with caching this layer if no updates to dependencies +COPY poetry.lock pyproject.toml /source/ +# --no-root declares not to install the project package since we're wanting to take advantage of caching dependency installation +# and the project is copied in and installed after this step +RUN poetry install --no-interaction --no-ansi --no-root + +# Copy in the rest of the source code and install local Nautobot plugin +COPY . /source +RUN poetry install --no-interaction --no-ansi + +COPY development/nautobot_config.py /opt/nautobot/nautobot_config.py diff --git a/nautobot-plugin-data-sync-servicenow/development/creds.example.env b/nautobot-plugin-data-sync-servicenow/development/creds.example.env new file mode 100644 index 000000000..94bbf310e --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/creds.example.env @@ -0,0 +1,10 @@ +POSTGRES_PASSWORD=notverysecurepwd +REDIS_PASSWORD=notverysecurepwd +SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj +NAUTOBOT_CREATE_SUPERUSER=true +NAUTOBOT_SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 +NAUTOBOT_SUPERUSER_PASSWORD=admin +# POSTGRES_HOST=localhost +# REDIS_HOST=localhost +# NAUTOBOT_ROOT=./development + diff --git a/nautobot-plugin-data-sync-servicenow/development/dev.env b/nautobot-plugin-data-sync-servicenow/development/dev.env new file mode 100644 index 000000000..eda6f4ec3 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/dev.env @@ -0,0 +1,17 @@ +ALLOWED_HOSTS=* +BANNER_TOP="Local" +CHANGELOG_RETENTION=0 +DEBUG=True +MAX_PAGE_SIZE=0 +METRICS_ENABLED=True +NAPALM_TIMEOUT=5 +NAUTOBOT_ROOT=/opt/nautobot +POSTGRES_DB=nautobot +POSTGRES_HOST=postgres +POSTGRES_USER=nautbot +REDIS_HOST=redis +REDIS_PORT=6379 +# REDIS_SSL=True +# Uncomment REDIS_SSL if using SSL +SUPERUSER_EMAIL=admin@example.com +SUPERUSER_NAME=admin diff --git a/nautobot-plugin-data-sync-servicenow/development/docker-compose.base.yml b/nautobot-plugin-data-sync-servicenow/development/docker-compose.base.yml new file mode 100644 index 000000000..31738f474 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/docker-compose.base.yml @@ -0,0 +1,32 @@ +--- +x-nautobot-build: &nautobot-build + build: + args: + NAUTOBOT_VER: "${NAUTOBOT_VER}" + PYTHON_VER: "${PYTHON_VER}" + context: "../" + dockerfile: "development/Dockerfile" +x-nautobot-base: &nautobot-base + image: "nautobot-data-sync-servicenow/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + env_file: + - "dev.env" + - "creds.env" + tty: true + +version: "3.4" +services: + nautobot: + ports: + - "8080:8080" + depends_on: + - "postgres" + - "redis" + <<: *nautobot-build + <<: *nautobot-base + worker: + entrypoint: "nautobot-server rqworker" + depends_on: + - "nautobot" + healthcheck: + disable: true + <<: *nautobot-base diff --git a/nautobot-plugin-data-sync-servicenow/development/docker-compose.dev.yml b/nautobot-plugin-data-sync-servicenow/development/docker-compose.dev.yml new file mode 100644 index 000000000..02e81f616 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/docker-compose.dev.yml @@ -0,0 +1,16 @@ +# We can't remove volumes in a compose override, for the test configuration using the final containers +# we don't want the volumes so this is the default override file to add the volumes in the dev case +# any override will need to include these volumes to use them. +# see: https://github.com/docker/compose/issues/3729 +--- +version: "3.4" +services: + nautobot: + command: "nautobot-server runserver 0.0.0.0:8080" + volumes: + - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" + - "../:/source" + worker: + volumes: + - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" + - "../:/source" diff --git a/nautobot-plugin-data-sync-servicenow/development/docker-compose.docs.yml b/nautobot-plugin-data-sync-servicenow/development/docker-compose.docs.yml new file mode 100644 index 000000000..a10cf4dfe --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/docker-compose.docs.yml @@ -0,0 +1,11 @@ +--- +version: "3.4" +services: + docs: + image: "nautobot-data-sync-servicenow/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" + volumes: + - "../docs:/source/docs:ro" + - "../mkdocs.yml:/source/mkdocs.yml:ro" + ports: + - "8001:8080" diff --git a/nautobot-plugin-data-sync-servicenow/development/docker-compose.requirements.yml b/nautobot-plugin-data-sync-servicenow/development/docker-compose.requirements.yml new file mode 100644 index 000000000..175cd297d --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/docker-compose.requirements.yml @@ -0,0 +1,25 @@ +--- +version: "3.4" +services: + postgres: + image: "postgres:13-alpine" + env_file: + - "dev.env" + - "creds.env" + volumes: + - "postgres_data:/var/lib/postgresql/data" + ports: + - "5432:5432" + redis: + image: "redis:6-alpine" + command: + - "sh" + - "-c" # this is to evaluate the $REDIS_PASSWORD from the env + - "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD" + env_file: + - "dev.env" + - "creds.env" + ports: + - "6379:6379" +volumes: + postgres_data: {} diff --git a/nautobot-plugin-data-sync-servicenow/development/nautobot_config.py b/nautobot-plugin-data-sync-servicenow/development/nautobot_config.py new file mode 100644 index 000000000..51dcb7fa8 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/development/nautobot_config.py @@ -0,0 +1,320 @@ +######################### +# # +# Required settings # +# # +######################### + +import os +import sys + +from distutils.util import strtobool +from django.core.exceptions import ImproperlyConfigured +from nautobot.core import settings + +# Enforce required configuration parameters +for key in [ + "ALLOWED_HOSTS", + "POSTGRES_DB", + "POSTGRES_USER", + "POSTGRES_HOST", + "POSTGRES_PASSWORD", + "REDIS_HOST", + "REDIS_PASSWORD", + "SECRET_KEY", +]: + if not os.environ.get(key): + raise ImproperlyConfigured(f"Required environment variable {key} is missing.") + + +def is_truthy(arg): + """Convert "truthy" strings into Booleans. + + Examples: + >>> is_truthy('yes') + True + Args: + arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, + f, false, off and 0. Raises ValueError if val is anything else. + """ + if isinstance(arg, bool): + return arg + return bool(strtobool(arg)) + + +TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" + +# This is a list of valid fully-qualified domain names (FQDNs) for the Nautobot server. Nautobot will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['nautobot.example.com', 'nautobot.internal.local'] +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(" ") + +# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: +# https://docs.djangoproject.com/en/stable/ref/settings/#databases +DATABASES = { + "default": { + "NAME": os.getenv("POSTGRES_DB", "nautobot"), # Database name + "USER": os.getenv("POSTGRES_USER", ""), # Database username + "PASSWORD": os.getenv("POSTGRES_PASSWORD", ""), # Datbase password + "HOST": os.getenv("POSTGRES_HOST", "localhost"), # Database server + "PORT": os.getenv("POSTGRES_PORT", ""), # Database port (leave blank for default) + "CONN_MAX_AGE": os.getenv("POSTGRES_TIMEOUT", 300), # Database timeout + "ENGINE": "django.db.backends.postgresql", # Database driver (Postgres only supported!) + } +} + +# Redis variables +REDIS_HOST = os.getenv("REDIS_HOST", "localhost") +REDIS_PORT = os.getenv("REDIS_PORT", 6379) +REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") + +# Check for Redis SSL +REDIS_SCHEME = "redis" +REDIS_SSL = is_truthy(os.environ.get("REDIS_SSL", False)) +if REDIS_SSL: + REDIS_SCHEME = "rediss" + +# The django-redis cache is used to establish concurrent locks using Redis. The +# django-rq settings will use the same instance/database by default. +# +# This "default" server is now used by RQ_QUEUES. +# >> See: nautobot.core.settings.RQ_QUEUES +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f"{REDIS_SCHEME}://{REDIS_HOST}:{REDIS_PORT}/0", + "TIMEOUT": 300, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "PASSWORD": REDIS_PASSWORD, + }, + } +} + +# RQ_QUEUES is not set here because it just uses the default that gets imported +# up top via `from nautobot.core.settings import *`. + +# REDIS CACHEOPS +CACHEOPS_REDIS = f"{REDIS_SCHEME}://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/1" + +# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. +# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and +# symbols. Nautobot will not run without this defined. For more information, see +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = os.environ["SECRET_KEY"] + + +######################### +# # +# Optional settings # +# # +######################### + +# Specify one or more name and email address tuples representing Nautobot administrators. These people will be notified of +# application errors (assuming correct email settings are provided). +ADMINS = [ + # ['John Doe', 'jdoe@example.com'], +] + +# URL schemes that are allowed within links in Nautobot +ALLOWED_URL_SCHEMES = ( + "file", + "ftp", + "ftps", + "http", + "https", + "irc", + "mailto", + "sftp", + "ssh", + "tel", + "telnet", + "tftp", + "vnc", + "xmpp", +) + +# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same +# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. +BANNER_TOP = os.environ.get("BANNER_TOP", "") +BANNER_BOTTOM = os.environ.get("BANNER_BOTTOM", "") + +# Text to include on the login page above the login form. HTML is allowed. +BANNER_LOGIN = os.environ.get("BANNER_LOGIN", "") + +# Cache timeout in seconds. Cannot be 0. Defaults to 900 (15 minutes). To disable caching, set CACHEOPS_ENABLED to False +CACHEOPS_DEFAULTS = {"timeout": 900} + +# Set to False to disable caching with cacheops. (Default: True) +CACHEOPS_ENABLED = True + +# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) +CHANGELOG_RETENTION = int(os.environ.get("CHANGELOG_RETENTION", 90)) + +# If True, all origins will be allowed. Other settings restricting allowed origins will be ignored. +# Defaults to False. Setting this to True can be dangerous, as it allows any website to make +# cross-origin requests to yours. Generally you'll want to restrict the list of allowed origins with +# CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGIN_REGEXES. +CORS_ORIGIN_ALLOW_ALL = is_truthy(os.environ.get("CORS_ORIGIN_ALLOW_ALL", False)) + +# A list of origins that are authorized to make cross-site HTTP requests. Defaults to []. +CORS_ALLOWED_ORIGINS = [ + # 'https://hostname.example.com', +] + +# A list of strings representing regexes that match Origins that are authorized to make cross-site +# HTTP requests. Defaults to []. +CORS_ALLOWED_ORIGIN_REGEXES = [ + # r'^(https?://)?(\w+\.)?example\.com$', +] + +# The file path where jobs will be stored. A trailing slash is not needed. Note that the default value of +# this setting is inside the invoking user's home directory. +# JOBS_ROOT = os.path.expanduser('~/.nautobot/jobs') + +# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal +# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging +# on a production system. +DEBUG = is_truthy(os.environ.get("DEBUG", False)) + +# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space +# within the global table (all prefixes and IP addresses not assigned to a VRF), set +# ENFORCE_GLOBAL_UNIQUE to True. +ENFORCE_GLOBAL_UNIQUE = is_truthy(os.environ.get("ENFORCE_GLOBAL_UNIQUE", False)) + +# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and +# by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. +EXEMPT_VIEW_PERMISSIONS = [ + # 'dcim.site', + # 'dcim.region', + # 'ipam.prefix', +] + +# HTTP proxies Nautobot should use when sending outbound HTTP requests (e.g. for webhooks). +# HTTP_PROXIES = { +# 'http': 'http://10.10.1.10:3128', +# 'https': 'http://10.10.1.10:1080', +# } + +# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing +# Nautobot from an internal IP. +INTERNAL_IPS = ("127.0.0.1", "::1") + +# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: +# https://docs.djangoproject.com/en/stable/topics/logging/ +LOGGING = {} + +# Setting this to True will display a "maintenance mode" banner at the top of every page. +MAINTENANCE_MODE = False + +# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. +# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request +# all objects by specifying "?limit=0". +MAX_PAGE_SIZE = int(os.environ.get("MAX_PAGE_SIZE", 1000)) + +# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that +# the default value of this setting is within the invoking user's home directory +# MEDIA_ROOT = os.path.expanduser('~/.nautobot/media') + +# By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the +# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: +# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' +# STORAGE_CONFIG = { +# 'AWS_ACCESS_KEY_ID': 'Key ID', +# 'AWS_SECRET_ACCESS_KEY': 'Secret', +# 'AWS_STORAGE_BUCKET_NAME': 'nautobot', +# 'AWS_S3_REGION_NAME': 'eu-west-1', +# } + +# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' +METRICS_ENABLED = False + +# Credentials that Nautobot will uses to authenticate to devices when connecting via NAPALM. +NAPALM_USERNAME = os.environ.get("NAPALM_USERNAME", "") +NAPALM_PASSWORD = os.environ.get("NAPALM_PASSWORD", "") + +# NAPALM timeout (in seconds). (Default: 30) +NAPALM_TIMEOUT = int(os.environ.get("NAPALM_TIMEOUT", 30)) + +# NAPALM optional arguments (see https://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must +# be provided as a dictionary. +NAPALM_ARGS = {} + +# Determine how many objects to display per page within a list. (Default: 50) +PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50)) + +# Enable installed plugins. Add the name of each plugin to the list. +PLUGINS = ["nautobot_data_sync_servicenow"] + +# Plugins configuration settings. These settings are used by various plugins that the user may have installed. +# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. +# PLUGINS_CONFIG = { +# 'my_plugin': { +# 'foo': 'bar', +# 'buzz': 'bazz' +# } +# } + +# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to +# prefer IPv4 instead. +PREFER_IPV4 = is_truthy(os.environ.get("PREFER_IPV4", False)) + +# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. +RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22 +RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220 + +# Remote authentication support +REMOTE_AUTH_ENABLED = False +REMOTE_AUTH_BACKEND = "nautobot.core.authentication.RemoteUserBackend" +REMOTE_AUTH_HEADER = "HTTP_REMOTE_USER" +REMOTE_AUTH_AUTO_CREATE_USER = True +REMOTE_AUTH_DEFAULT_GROUPS = [] +REMOTE_AUTH_DEFAULT_PERMISSIONS = {} + +# This determines how often the GitHub API is called to check the latest release of Nautobot. Must be at least 1 hour. +RELEASE_CHECK_TIMEOUT = 24 * 3600 + +# This repository is used to check whether there is a new release of Nautobot available. Set to None to disable the +# version check or use the URL below to check for release in the official Nautobot repository. +RELEASE_CHECK_URL = None +# RELEASE_CHECK_URL = 'https://api.github.com/repos/nautobot/nautobot/releases' + +# Maximum execution time for background tasks, in seconds. +RQ_DEFAULT_TIMEOUT = 300 + +# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to +# re-authenticate. (Default: 1209600 [14 days]) +SESSION_COOKIE_AGE = 1209600 # 2 weeks, in seconds + + +# By default, Nautobot will store session data in the database. Alternatively, a file path can be specified here to use +# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only +# database access.) Note that the user as which Nautobot runs must have read and write permissions to this path. +SESSION_FILE_PATH = None + +# Configure SSO, for more information see docs/configuration/authentication/sso.md +SOCIAL_AUTH_ENABLED = False + +# Time zone (default: UTC) +TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") + +# Date/time formatting. See the following link for supported formats: +# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date +DATE_FORMAT = os.environ.get("DATE_FORMAT", "N j, Y") +SHORT_DATE_FORMAT = os.environ.get("SHORT_DATE_FORMAT", "Y-m-d") +TIME_FORMAT = os.environ.get("TIME_FORMAT", "g:i a") +SHORT_TIME_FORMAT = os.environ.get("SHORT_TIME_FORMAT", "H:i:s") +DATETIME_FORMAT = os.environ.get("DATETIME_FORMAT", "N j, Y g:i a") +SHORT_DATETIME_FORMAT = os.environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") + +# A list of strings designating all applications that are enabled in this Django installation. Each string should be a dotted Python path to an application configuration class (preferred), or a package containing an application. +# https://nautobot.readthedocs.io/en/latest/configuration/optional-settings/#extra-applications +EXTRA_INSTALLED_APPS = os.environ["EXTRA_INSTALLED_APPS"].split(",") if os.environ.get("EXTRA_INSTALLED_APPS") else [] + +# Django Debug Toolbar +DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING} + +if "debug_toolbar" not in EXTRA_INSTALLED_APPS: + EXTRA_INSTALLED_APPS.append("debug_toolbar") +if "debug_toolbar.middleware.DebugToolbarMiddleware" not in settings.MIDDLEWARE: + settings.MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/nautobot-plugin-data-sync-servicenow/docs/extra.css b/nautobot-plugin-data-sync-servicenow/docs/extra.css new file mode 100644 index 000000000..6a95f356a --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/docs/extra.css @@ -0,0 +1,19 @@ +/* Images */ +img { + display: block; + margin-left: auto; + margin-right: auto; +} + +/* Tables */ +table { + margin-bottom: 24px; + width: 100%; +} +th { + background-color: #f0f0f0; + padding: 6px; +} +td { + padding: 6px; +} diff --git a/nautobot-plugin-data-sync-servicenow/docs/index.md b/nautobot-plugin-data-sync-servicenow/docs/index.md new file mode 100644 index 000000000..3dccb8253 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/docs/index.md @@ -0,0 +1,17 @@ +# NautobotDataSyncServicenow + +TODO: Write plugin documentation, the outline here is provided as a guide and should be expanded upon. If more detail is required you are encouraged to expand on the table of contents (TOC) in `mkdocs.yml` to add additional pages. + +## Description + +## Installation + +## Configuration + +## Usage + +## API + +## Views + +## Models diff --git a/nautobot-plugin-data-sync-servicenow/docs/requirements.txt b/nautobot-plugin-data-sync-servicenow/docs/requirements.txt new file mode 100644 index 000000000..f354292fd --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs==1.1.2 +markdown-include==0.6.0 diff --git a/nautobot-plugin-data-sync-servicenow/invoke.example.yml b/nautobot-plugin-data-sync-servicenow/invoke.example.yml new file mode 100644 index 000000000..98483cdca --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/invoke.example.yml @@ -0,0 +1,11 @@ +--- +nautobot_data_sync_servicenow: + project_name: "nautobot-data-sync-servicenow" + nautobot_ver: "develop-latest" + local: false + python_ver: "3.6" + compose_dir: "development" + compose_files: + - "docker-compose.requirements.yml" + - "docker-compose.base.yml" + - "docker-compose.dev.yml" diff --git a/nautobot-plugin-data-sync-servicenow/mkdocs.yml b/nautobot-plugin-data-sync-servicenow/mkdocs.yml new file mode 100644 index 000000000..b51462d72 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/mkdocs.yml @@ -0,0 +1,25 @@ +--- +dev_addr: "127.0.0.1:8001" +edit_uri: "edit/main/nautobot-plugin-data-sync-servicenow/docs" +site_name: "NautobotDataSyncServicenow Documentation" +site_url: "https://nautobot-plugin-data-sync-servicenow.readthedocs.io/" +repo_url: "https://github.com/nautobot/nautobot-plugin-data-sync-servicenow" +python: + install: + - requirements: "docs/requirements.txt" +theme: + name: "readthedocs" + navigation_depth: 4 + hljs_languages: + - "django" + - "yaml" +extra_css: + - "extra.css" +markdown_extensions: + - "admonition" + - markdown_include.include: + headingOffset: 1 + - toc: + permalink: true +nav: + - Introduction: "index.md" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py new file mode 100644 index 000000000..74ca84c9d --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py @@ -0,0 +1,24 @@ +"""Plugin declaration for nautobot_data_sync_servicenow.""" + +__version__ = "0.1.0" + +from nautobot.extras.plugins import PluginConfig + + +class NautobotDataSyncServicenowConfig(PluginConfig): + """Plugin configuration for the nautobot_data_sync_servicenow plugin.""" + + name = "nautobot_data_sync_servicenow" + verbose_name = "Nautobot ServiceNow Data Synchronization" + version = __version__ + author = "Network to Code, LLC" + description = "Nautobot ServiceNow Data Synchronization." + base_url = "data-sync-servicenow" + required_settings = [] + min_version = "1.0.0" + max_version = "1.9999" + default_settings = {} + caching_config = {} + + +config = NautobotDataSyncServicenowConfig # pylint:disable=invalid-name diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py new file mode 100644 index 000000000..93737379f --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py @@ -0,0 +1 @@ +"""REST API module for nautobot_data_sync_servicenow plugin.""" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/data/mappings.yaml b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/data/mappings.yaml new file mode 100644 index 000000000..4e59eaa8a --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/data/mappings.yaml @@ -0,0 +1,59 @@ +--- +- table: "cmn_location" + modelname: "location" + mappings: + - field: "name" + column: "name" + - field: "parent_location_name" + reference: + key: "parent" + table: "cmn_location" + column: "name" +- table: "cmdb_ci_ip_switch" + modelname: "device" + parent: + modelname: "location" + field: "location_name" + column: "location" + mappings: + - field: "name" + column: "name" + - field: "location_name" + reference: + key: "location" + table: "cmn_location" + column: "name" + - field: "model" + reference: + key: "model_id" + table: "cmdb_model" + column: "name" + - field: "platform" + reference: + key: "{{ app_prefix }}platform" + table: "{{ app_prefix }}platform" + column: "name" + - field: "role" + reference: + key: "{{ app_prefix }}device_role" + table: "{{ app_prefix }}device_role" + column: "name" + - field: "vendor" + reference: + key: "manufacturer" + table: "core_company" + column: "name" +- table: "cmdb_ci_network_adapter" + modelname: "interface" + parent: + modelname: "device" + field: "device_name" + column: "cmdb_ci" + mappings: + - field: "name" + column: "name" + - field: "device_name" + reference: + key: "cmdb_ci" + table: "cmdb_ci_ip_switch" + column: "name" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_nautobot.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_nautobot.py new file mode 100644 index 000000000..a5a8c6eac --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_nautobot.py @@ -0,0 +1,86 @@ +"""DiffSync adapter class for Nautobot as source-of-truth.""" + +from diffsync import DiffSync +from diffsync.exceptions import ObjectNotFound + +from nautobot.dcim.models import Device, Interface, Region, Site + +from . import models + + +class NautobotDiffSync(DiffSync): + """Nautobot adapter for DiffSync.""" + + location = models.Location + device = models.Device + interface = models.Interface + + top_level = [ + "location", + ] + + def load_regions(self, parent_location=None): + """Recursively add Nautobot Region objects as DiffSync Location models.""" + parent_pk = parent_location.pk if parent_location else None + for region_record in Region.objects.filter(parent=parent_pk): + location = self.location(diffsync=self, name=region_record.name, pk=region_record.pk) + if parent_location: + parent_location.contained_locations.append(location) + location.parent_location_name = parent_location.name + self.add(location) + self.load_regions(parent_location=location) + + def load_sites(self): + """Add Nautobot Site objects as DiffSync Location models.""" + for site_record in Site.objects.all(): + # A Site and a Region may share the same name; if so they become part of the same Location record. + try: + location = self.get(self.location, site_record.name) + except ObjectNotFound: + location = self.location(diffsync=self, name=site_record.name) + self.add(location) + location.status = site_record.status + if site_record.region: + if location.name != site_record.region.name: + region_location = self.get(self.location, site_record.region.name) + region_location.contained_locations.append(location) + location.parent_location_name = region_location.name + + def load_interface(self, interface_record, device_model): + """Import a single Nautobot Interface object as a DiffSync Interface model.""" + interface = self.interface( + diffsync=self, + name=interface_record.name, + device_name=device_model.name, + description=interface_record.description, + pk=interface_record.pk, + ) + self.add(interface) + device_model.add_child(interface) + + def load(self): + """Load data from Nautobot.""" + # Import all Nautobot Region records as Locations + self.load_regions(parent_location=None) + + # Import all Nautobot Site records as Locations + self.load_sites() + + for location in self.get_all(self.location): + for device_record in Device.objects.filter(site=location.remote_site_id): + device = self.device( + diffsync=self, + name=device_record.name, + platform=str(device_record.platform) if device_record.platform else None, + model=str(device_record.device_type), + role=str(device_record.device_role), + location_name=location.name, + vendor=str(device_record.device_type.manufacturer), + status=device_record.status, + pk=device_record.pk, + ) + self.add(device) + location.add_child(device) + + for interface_record in Interface.objects.filter(device=device_record): + self.load_interface(interface_record, device) diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_servicenow.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_servicenow.py new file mode 100644 index 000000000..08505f8cf --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/adapter_servicenow.py @@ -0,0 +1,146 @@ +"""DiffSync adapter for ServiceNow.""" +import os + +from diffsync import DiffSync +from diffsync.exceptions import ObjectAlreadyExists +from jinja2 import Environment, FileSystemLoader +import yaml + +from . import models + + +class ServiceNowDiffSync(DiffSync): + """DiffSync adapter using pysnow to communicate with a ServiceNow server.""" + + location = models.Location + device = models.Device + interface = models.Interface + + top_level = [ + "location", + ] + + DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")) + + def __init__(self, *args, client=None, worker=None, **kwargs): + super().__init__(*args, **kwargs) + self.client = client + self.worker = worker + self.sys_ids = {} + self.mapping_data = [] + + def load(self): + """Load data via pysnow.""" + self.mapping_data = self.load_yaml_datafile("mappings.yaml", {"app_prefix": self.client.app_prefix}) + + for entry in self.mapping_data: + self.load_table(**entry) + + @classmethod + def load_yaml_datafile(cls, filename, config): + """Get the contents of the given YAML data file. + + Args: + filename (str): Filename within the 'data' directory. + config (dict): Data for Jinja2 templating. + """ + file_path = os.path.join(cls.DATA_DIR, filename) + if not os.path.isfile(file_path): + raise RuntimeError(f"No data file found at {file_path}") + env = Environment(loader=FileSystemLoader(cls.DATA_DIR), autoescape=True) + template = env.get_template(filename) + populated = template.render(config) + return yaml.safe_load(populated) + + def load_table(self, modelname, table, mappings, **kwargs): + """Load data from the ServiceNow "table" into the DiffSync model. + + Args: + modelname (str): DiffSync model class identifier, such as "location" or "device". + table (str): ServiceNow table name, such as "cmdb_ci_ip_switch" + mappings (list): List of dicts, each stating how to populate a field in the model. + **kwargs: Optional arguments, all of which default to False if unset: + + - parent (dict): Dict of {"modelname": ..., "field": ...} used to link table records back to their parents + """ + model_cls = getattr(self, modelname) + self.worker.job_log(f"Loading table {table} into {modelname} instances...") + + if "parent" not in kwargs: + # Load the entire table + for record in self.client.all_table_entries(table): + self.load_record(table, record, model_cls, mappings, **kwargs) + else: + # Load items per parent object that we know/care about + # This is necessary because, for example, the cmdb_ci_network_adapter table contains network interfaces + # for ALL types of devices (servers, switches, firewalls, etc.) but we only have switches as parent objects + for parent in self.get_all(kwargs["parent"]["modelname"]): + self.worker.job_log(f"Loading children of {parent}") + for record in self.client.all_table_entries(table, {kwargs["parent"]["column"]: parent.sys_id}): + self.load_record(table, record, model_cls, mappings, **kwargs) + + def load_record(self, table, record, model_cls, mappings, **kwargs): + """Helper method to load_table().""" + self.sys_ids.setdefault(table, {})[record["sys_id"]] = record + + ids_attrs = self.map_record_to_attrs(record, mappings) + model = model_cls(**ids_attrs) + modelname = model.get_type() + + try: + self.add(model) + self.worker.job_log(f"Added {modelname} {model.get_unique_id()}") + except ObjectAlreadyExists: + # TODO: the baseline data in ServiceNow has a number of duplicate Location entries. For now, continue + self.worker.job_log(f"Duplicate object encountered for {modelname} {model.get_unique_id()}") + + if "parent" in kwargs: + parent_uid = getattr(model, kwargs["parent"]["field"]) + if parent_uid is None: + self.worker.job_log( + f"Model {modelname} {model.get_unique_id} does not have a parent uid value in field {kwargs['parent']['field']}" + ) + else: + parent_model = self.get(kwargs["parent"]["modelname"], parent_uid) + parent_model.add_child(model) + self.worker.job_log( + f"Added {modelname} {model.get_unique_id} as a child of {parent_model.get_type()} {parent_model.get_unique_id()}" + ) + + def map_record_to_attrs(self, record, mappings): # TODO pylint: disable=too-many-branches + """Helper method to load_table().""" + attrs = {"sys_id": record["sys_id"]} + for mapping in mappings: + value = None + if "column" in mapping: + value = record[mapping["column"]] + elif "reference" in mapping: + # Reference by sys_id to a field in a record in another table + table = mapping["reference"]["table"] + if "key" in mapping["reference"]: + key = mapping["reference"]["key"] + if key not in record: + self.worker.job_log(f"Key {key} is not present in record {record}") + else: + sys_id = record[key] + else: + raise NotImplementedError + + if sys_id: + if sys_id not in self.sys_ids.get(table, {}): + referenced_record = self.client.get_by_sys_id(table, sys_id) + if referenced_record is None: + self.worker.job_log( + f"Record references sys_id {sys_id}, but that was not found in table {table}" + ) + else: + self.sys_ids.setdefault(table, {})[sys_id] = referenced_record + + if sys_id in self.sys_ids.get(table, {}): + value = self.sys_ids[table][sys_id][mapping["reference"]["column"]] + else: + raise NotImplementedError + + attrs[mapping["field"]] = value + + return attrs diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/models.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/models.py new file mode 100644 index 000000000..6b3ada83c --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/diffsync/models.py @@ -0,0 +1,198 @@ +"""DiffSyncModel subclasses for Nautobot-to-ServiceNow data sync.""" +from typing import List, Optional +import uuid + +from diffsync import DiffSyncModel +import pysnow + + +class ServiceNowCRUDMixin: + """Mixin class for all ServiceNow models, to support CRUD operations based on mappings.yaml.""" + + def map_data_to_sn_record(self, data, mapping_entry, existing_record=None): + """Map create/update data from DiffSync to a corresponding ServiceNow data record.""" + record = existing_record or {} + for mapping in mapping_entry.get("mappings", []): + if mapping["field"] not in data: + continue + value = data[mapping["field"]] + if "column" in mapping: + record[mapping["column"]] = value + elif "reference" in mapping: + tablename = mapping["reference"]["table"] + target = None + if "column" in mapping["reference"]: + if value is not None: + target = self.diffsync.client.get_by_query(tablename, {mapping["reference"]["column"]: value}) + if target is None: + self.diffsync.worker.job_log(f"Unable to find reference target in {tablename}") + else: + raise NotImplementedError + + sys_id = target["sys_id"] if target else None + record[mapping["reference"]["key"]] = sys_id + else: + raise NotImplementedError + + self.diffsync.worker.job_log(f"Mapped data {data} to record {record}") + return record + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create a new instance, data-driven by mappings.""" + entry = None + for item in diffsync.mapping_data: + if item["modelname"] == cls.get_type(): + entry = item + break + + if not entry: + raise RuntimeError(f"Did not find a mapping entry for {cls.get_type()}!") + + model = super().create(diffsync, ids=ids, attrs=attrs) + + sn_resource = diffsync.client.resource(api_path=f"/table/{entry['table']}") + sn_record = model.map_data_to_sn_record(data=dict(**ids, **attrs), mapping_entry=entry) + sn_resource.create(payload=sn_record) + + return model + + def update(self, attrs): + """Update an existing instance, data-driven by mappings.""" + entry = None + for item in self.diffsync.mapping_data: + if item["modelname"] == self.get_type(): + entry = item + break + + if not entry: + raise RuntimeError("Did not find a mapping entry for {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: + record = sn_resource.get(query=query).one() + except pysnow.exceptions.MultipleResults: + self.diffsync.worker.job_log( + f"Unsure which record to update, as query {query} matched more than one item in table {entry['table']}" + ) + return None + + sn_record = self.map_data_to_sn_record(data=attrs, mapping_entry=entry, existing_record=record) + sn_resource.update(query=query, payload=sn_record) + + super().update(attrs) + return self + + # TODO delete() method + +class Location(ServiceNowCRUDMixin, DiffSyncModel): + """ServiceNow Location model.""" + + _modelname = "location" + _identifiers = ("name",) + _attributes = ("parent_location_name",) + _children = { + "device": "devices", + } + + name: str + + parent_location_name: Optional[str] + contained_locations: List["Location"] = list() + + devices: List["Device"] = list() + + sys_id: Optional[str] = None + pk: Optional[uuid.UUID] = None + + +class Device(ServiceNowCRUDMixin, DiffSyncModel): + """ServiceNow Device model.""" + + _modelname = "device" + _identifiers = ("name",) + # For now we do not store more of the device fields in ServiceNow: + # platform, model, role, vendor + # ...as we would need to sync these data models to ServiceNow as well, and we don't do that yet. + _attributes = ( + "location_name", + ) + _children = { + "interface": "interfaces", + } + + name: str + + location_name: Optional[str] + model: Optional[str] + platform: Optional[str] + role: Optional[str] + vendor: Optional[str] + + interfaces: List["Interface"] = list() + + sys_id: Optional[str] = None + pk: Optional[uuid.UUID] = None + + +class Interface(ServiceNowCRUDMixin, DiffSyncModel): + """ServiceNow Interface model.""" + + _modelname = "interface" + _identifiers = ( + "device_name", + "name", + ) + _shortname = ("name",) + # ServiceNow currently stores very little data about interfaces that we are interested in + _attributes = () + + _children = {"ip_address": "ip_addresses"} + + name: str + device_name: str + + access_vlan: Optional[int] + active: Optional[bool] + allowed_vlans: List[str] = list() + description: Optional[str] + is_virtual: Optional[bool] + is_lag: Optional[bool] + is_lag_member: Optional[bool] + lag_members: List[str] = list() + mode: Optional[str] # TRUNK, ACCESS, L3, NONE + mtu: Optional[int] + parent: Optional[str] + speed: Optional[int] + switchport_mode: Optional[str] + type: Optional[str] + + ip_addresses: List["IPAddress"] = list() + + sys_id: Optional[str] = None + pk: Optional[uuid.UUID] = None + + +class IPAddress(ServiceNowCRUDMixin, DiffSyncModel): + """An IPv4 or IPv6 address.""" + + _modelname = "ip_address" + _identifiers = ("address",) + _attributes = ( + "device_name", + "interface_name", + ) + + address: str # TODO: change to netaddr.IPAddress? + + device_name: Optional[str] + interface_name: Optional[str] + + sys_id: Optional[str] = None + pk: Optional[uuid.UUID] = None + + +Location.update_forward_refs() +Device.update_forward_refs() +Interface.update_forward_refs() diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/migrations/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/servicenow.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/servicenow.py new file mode 100644 index 000000000..bf91be9dc --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/servicenow.py @@ -0,0 +1,165 @@ +"""Interactions with ServiceNow APIs.""" +import logging +import os + +from pysnow import Client +import requests + + +logger = logging.getLogger(__name__) + + +def python_value_to_string(value): + """The ServiceNow REST API represents everything as a string, so map Python values to their API representation.""" + if value is None: + value = "" + elif isinstance(value, bool): + value = str(value).lower() + else: + value = str(value) + return value + + +class ServiceNowClient(Client): + """Extend the pysnow Client with additional use-case-specific functionality.""" + + def __init__(self, instance="", username="", password="", app_prefix="", worker=None): + """Create a ServiceNowClient with the appropriate environment parameters.""" + super().__init__(instance=instance, user=username, password=password) + + self.worker = worker + self.app_prefix = app_prefix + + # When getting records from ServiceNow, for reference fields, only return the sys_id value of the reference, + # rather than returning a dict of {"link": "https://.servicenow.com/...", "value": } + # We don't need the link for our purposes, and including it makes it harder to preserve idempotence. + self.parameters.exclude_reference_link = True + + def all_table_entries(self, table, query=None): + """Iterator over all records in a given table.""" + if not query: + query = {} + logger.debug("Getting all entries in table %s matching query %s", table, query) + yield from self.resource(api_path=f"/table/{table}").get(query=query, stream=True).all() + + def get_by_sys_id(self, table, sys_id): + """Get a record with a given sys_id from a given table.""" + return self.get_by_query(table, {"sys_id": sys_id}) + + def get_by_query(self, table, query): + """Get a specific record from a given table.""" + logger.debug("Querying table %s with query %s", table, query) + try: + result = self.resource(api_path=f"/table/{table}").get(query=query).one_or_none() + except requests.exceptions.HTTPError as exc: + # Raised if for example we get a 400 response because we're querying a nonexistent table + logger.error("HTTP error encountered: %s", exc) + result = None + + if not result: + logger.warning("Query %s did not match an object in table %s", query, table) + return result + + def ensure_choice(self, table, element, label, value=None): + """Ensure that the given choice value exists for the given element of the given table. + + Args: + table (str): Name of the table this choice belongs to + element (str): Name of the table field this choice belongs to + label (str): Human-readable label for this choice value. + value (str): Backend value of this choice. + """ + if not value: + value = label + + sys_choice = self.resource(api_path="/table/sys_choice") + query = {"name": table, "element": element, "label": label, "value": value} + record = sys_choice.get(query=query).one_or_none() + if record: + self.worker.unchanged("No changes to choice %r for field %r in table %r", label, element, table) + else: + if not self.worker.dry_run: + sys_choice.create(payload=query) + self.worker.created("Created choice %r for field %r in table %r", label, element, table) + + def ensure_field(self, table, field, datatype, **kwargs): + """Ensure that the given custom field exists in the given table. + + Args: + table (str): Name of the table to inspect/modify + field (str): Slug of the field to ensure + datatype (str): Datatype human-readable name ("string", "longint", etc.) as defined by ServiceNow. + **kwargs: Additional parameters to set on the field (``column_label``, ``max_length``, etc.) + """ + if "column_label" not in kwargs: + kwargs["column_label"] = field.replace("_", " ").title() + + # In all the examples I've found online, people are setting `internal_type` to a sys_id value. + # The below is how to find the sys_id for a given type such as string, longint, reference, etc. + # However, in my experimentation, just using the type name string seems to work just as well + # and is less error-prone. + # + # sys_glide = self.resource(api_path="/table/sys_glide_object") + # datatype_sys_id_record = sys_glide.get(query={"label": datatype.title()}).one_or_none() + # if not datatype_sys_id_record: + # logger.error("No datatype %r found", datatype) + # return + # datatype_sys_id = datatype_sys_id_record["sys_id"] + + sys_dict = self.resource(api_path="/table/sys_dictionary") + query = {"name": table, "element": field} + updates = {"internal_type": datatype, **kwargs} + record = sys_dict.get(query=query).one_or_none() + if record: + changed = update(record, updates) + if changed: + if not self.worker.dry_run: + record = sys_dict.update(query=query, payload=record) + self.worker.updated("Updated existing field %r in table %r", field, table) + else: + self.worker.unchanged("No changes to field %r in table %r", field, table) + else: + if not self.worker.dry_run: + record = sys_dict.create(payload={**query, **updates}) + self.worker.created("Added field %r to table %r", field, table) + + # TODO: can we also automatically add this field to the form layout for this table? + + def ensure_table(self, table_name, label=None, fields=()): + """Ensure that the given custom table exists and is correctly defined. + + Args: + table_name (str): Table slug + label (str): Human-readable label for this table + fields (list): List of (name, type, kwargs, choices) to pass through to :meth:`ensure_field` + """ + if not label: + label = table_name.upper() + + sys_db = self.resource(api_path="/table/sys_db_object") + query = {"name": table_name} + updates = {"label": label} + record = sys_db.get(query=query).one_or_none() + if record: + changed = update(record, updates) + if changed: + if not self.worker.dry_run: + record = sys_db.update(query=query, payload=record) + self.worker.updated("Updated existing table %r", table_name) + else: + self.worker.unchanged("No changes to definition of table %r", table_name) + else: + if not self.worker.dry_run: + record = sys_db.create(payload={**query, **updates}) + self.worker.created("Created table %s", table_name) + + for slug, datatype, kwargs, choices in fields: + # Create fields in this table. + self.ensure_field(table_name, slug, datatype, **kwargs) + if datatype == "choice": + for choice in choices: + self.ensure_choice(table_name, slug, choice) + elif choices: + logger.error("Choices are specified for non-choice field %r of type %s", slug, datatype) + + # TODO: can we also automatically add a link to this table view? diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py new file mode 100644 index 000000000..b42edcee2 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py @@ -0,0 +1 @@ +"""Unit tests for nautobot_data_sync_servicenow plugin.""" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_api.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_api.py new file mode 100644 index 000000000..b2daf0743 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_api.py @@ -0,0 +1,28 @@ +"""Unit tests for nautobot_data_sync_servicenow.""" +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from nautobot.users.models import Token + +User = get_user_model() + + +class PlaceholderAPITest(TestCase): + """Test the NautobotDataSyncServicenow API.""" + + def setUp(self): + """Create a superuser and token for API calls.""" + self.user = User.objects.create(username="testuser", is_superuser=True) + self.token = Token.objects.create(user=self.user) + self.client = APIClient() + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + + def test_placeholder(self): + """Verify that devices can be listed.""" + url = reverse("dcim-api:device-list") + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 0) diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_basic.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_basic.py new file mode 100644 index 000000000..2b3f4b58d --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/test_basic.py @@ -0,0 +1,16 @@ +"""Basic tests that do not require Django.""" +import unittest +import os +import toml + +from nautobot_data_sync_servicenow import __version__ as project_version + + +class TestVersion(unittest.TestCase): + """Test Version is the same.""" + + def test_version(self): + """Verify that pyproject.toml version is same as version specified in the package.""" + parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"] + self.assertEqual(project_version, poetry_version) diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py new file mode 100644 index 000000000..539f60711 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py @@ -0,0 +1,4 @@ +"""Django urlpatterns declaration for nautobot_data_sync_servicenow plugin.""" +# from django.urls import path + +urlpatterns = [] diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/utilities.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/utilities.py new file mode 100644 index 000000000..de99d7ce4 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/utilities.py @@ -0,0 +1,93 @@ +import os + +import usaddress +import yaml + +from jinja2 import Environment, FileSystemLoader + + +DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) + + +def get_nested_attr(obj, attr): + """Like getattr, but handles nested attributes like "device_type.manufacturer.name". + + Additionally, returns None if no such attribute exists rather than throwing an exception. + """ + value = obj + for subattr in attr.split("."): + if hasattr(value, subattr): + value = getattr(value, subattr) + else: + return None + return value + + +def update(target, source): + """Update the target dictionary with data from source. + + Equivalent to calling target.update(source), but reports whether any changes occurred in target. + + Args: + target (dict): Dictionary to update + source (dict): Dictionary whose contents will be set into ``target``. + + Returns: + bool: True if ``target`` was modified at all. + """ + changed = False + for key, value in source.items(): + if key in target: + if target[key] == value: + continue # No change to this key + target[key] = value + changed = True + + return changed + + +def load_yaml_datafile(filename, config): + """Get the contents of the given YAML datafile. + + Args: + filename (str): Filename within the ``nautobot_data_sync_servicenow/data/`` directory. + config (dict): Data for Jinja2 templating. + + Returns: + object: Parsed and populated data. + """ + file_path = os.path.join(DATA_DIR, filename) + if not os.path.isfile(file_path): + raise RuntimeError(f"No data file found at {file_path}") + env = Environment(loader=FileSystemLoader(DATA_DIR), autoescape=True) + template = env.get_template(filename) + populated = template.render(config) + return yaml.safe_load(populated) + + +def parse_physical_address(nb_site, field): + """Attempt to parse the free-text Nautobot "site.physical_address" into tokens and construct the requested field. + + Args: + nb_site (Site): Nautobot record that has a "physical_address". + field (str): Address field to retrieve. + """ + # usaddress.tag() returns a tuple (data, address_type) + data, _ = usaddress.tag(nb_site.physical_address) + if field == "street": + text = "" + for key in ("AddressNumber", "StreetName", "StreetNamePostType", "OccupancyType", "OccupancyIdentifier"): + if key in data: + if text: + text += " " + text += data[key] + return text + if field == "city": + return data.get("PlaceName", "") + if field == "state": + return data.get("StateName", "") + if field == "zip": + return data.get("ZipCode", "") + if field == "country": + return data.get("CountryName", "") + raise NotImplementedError diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/worker.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/worker.py new file mode 100644 index 000000000..7610b7b08 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/worker.py @@ -0,0 +1,66 @@ +import logging + +from diffsync.enum import DiffSyncFlags + +from nautobot.extras.jobs import StringVar + +from nautobot_data_sync.sync.base import DataSyncWorker + +from .diffsync.adapter_nautobot import NautobotDiffSync +from .diffsync.adapter_servicenow import ServiceNowDiffSync +from .servicenow import ServiceNowClient + + +class ServiceNowExportDataSyncWorker(DataSyncWorker): + """Worker class to handle data sync to ServiceNow.""" + + snow_instance = StringVar( + label="ServiceNow instance", + description='<instance>.servicenow.com, such as "dev12345"' + ) + snow_username = StringVar( + label="ServiceNow username", + ) + snow_password = StringVar( + label="ServiceNow password", + # TODO widget=... + ) + snow_app_prefix = StringVar( + label="ServiceNow app prefix", + description="(if any)", + default="", + required=False, + ) + + class Meta: + name = "Data Sync to ServiceNow" + slug = "service-now-export" + description = "Synchronize data from Nautobot into ServiceNow." + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.snc = ServiceNowClient( + instance=self.data["snow_instance"], + username=self.data["snow_username"], + password=self.data["snow_password"], + app_prefix=self.data["snow_app_prefix"], + worker=self, + ) + self.servicenow_diffsync = ServiceNowDiffSync(client=self.snc, worker=self) + self.nautobot_diffsync = NautobotDiffSync() + + def execute(self): + """Sync a slew of Nautobot data into ServiceNow.""" + self.servicenow_diffsync.load() + self.nautobot_diffsync.load() + + diff = self.servicenow_diffsync.diff_from(self.nautobot_diffsync) + self.sync.diff = diff.dict() + self.sync.save() + if not self.dry_run: + self.servicenow_diffsync.sync_from( + self.nautobot_diffsync, + flags=DiffSyncFlags.CONTINUE_ON_FAILURE | + DiffSyncFlags.LOG_UNCHANGED_RECORDS | + DiffSyncFlags.SKIP_UNMATCHED_DST, + ) diff --git a/nautobot-plugin-data-sync-servicenow/poetry.lock b/nautobot-plugin-data-sync-servicenow/poetry.lock new file mode 100644 index 000000000..d2e6ea395 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/poetry.lock @@ -0,0 +1,1362 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asgiref" +version = "3.3.4" +description = "ASGI specs, helper code, and adapters" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "astroid" +version = "2.5.6" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +wrapt = ">=1.11,<1.13" + +[[package]] +name = "bandit" +version = "1.7.0" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "dataclasses" +version = "0.7" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = ">=3.6, <3.7" + +[[package]] +name = "diffsync" +version = "1.3.0" +description = "Library to easily sync/diff/update 2 different data sources" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +colorama = ">=0.4.3,<0.5.0" +dataclasses = {version = ">=0.7,<0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} +pydantic = ">=1.7.2,<2.0.0" +structlog = ">=20.1.0,<21.0.0" + +[[package]] +name = "django" +version = "3.1.11" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asgiref = ">=3.2.10,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-debug-toolbar" +version = "3.2.1" +description = "A configurable set of panels that display various debug information about the current request/response." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" +sqlparse = ">=0.2.0" + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[package]] +name = "gitpython" +version = "3.1.17" +description = "Python Git Library" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "ijson" +version = "2.6.1" +description = "Iterative JSON parser with a standard Python iterator interface" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "importlib-metadata" +version = "3.4.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "invoke" +version = "1.5.0" +description = "Pythonic task execution" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.8.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +name = "joblib" +version = "1.0.1" +description = "Lightweight pipelining with Python functions" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "lazy-object-proxy" +version = "1.6.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "livereload" +version = "2.6.3" +description = "Python LiveReload is an awesome tool for web developers" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" +tornado = {version = "*", markers = "python_version > \"2.7\""} + +[[package]] +name = "lunr" +version = "0.5.8" +description = "A Python implementation of Lunr.js" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +future = ">=0.16.0" +nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} +six = ">=1.11.0" + +[package.extras] +languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] + +[[package]] +name = "markdown" +version = "3.3.4" +description = "Python implementation of Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-include" +version = "0.6.0" +description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +markdown = "*" + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mkdocs" +version = "1.1.2" +description = "Project documentation with Markdown." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +click = ">=3.3" +Jinja2 = ">=2.10.1" +livereload = ">=2.5.1" +lunr = {version = "0.5.8", extras = ["languages"]} +Markdown = ">=3.2.1" +PyYAML = ">=3.10" +tornado = ">=5.0" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nltk" +version = "3.6.2" +description = "Natural Language Toolkit" +category = "dev" +optional = false +python-versions = ">=3.5.*" + +[package.dependencies] +click = "*" +joblib = "*" +regex = "*" +tqdm = "*" + +[package.extras] +all = ["matplotlib", "twython", "scipy", "numpy", "gensim (<4.0.0)", "python-crfsuite", "pyparsing", "scikit-learn", "requests"] +corenlp = ["requests"] +machine_learning = ["gensim (<4.0.0)", "numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + +[[package]] +name = "oauthlib" +version = "3.1.0" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +rsa = ["cryptography"] +signals = ["blinker"] +signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pbr" +version = "5.6.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "probableparsing" +version = "0.0.1" +description = "Common methods for propbable parsers" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.7.4" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] + +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pylint" +version = "2.8.2" +description = "python code static checker" +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +astroid = ">=2.5.6,<2.7" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +name = "pylint-django" +version = "2.4.4" +description = "A Pylint plugin to help Pylint understand the Django web framework" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pylint = ">=2.0" +pylint-plugin-utils = ">=0.5" + +[package.extras] +for_tests = ["django-tables2", "factory-boy", "coverage", "pytest"] +with_django = ["django"] + +[[package]] +name = "pylint-plugin-utils" +version = "0.6" +description = "Utilities and helpers for writing Pylint plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pysnow" +version = "0.7.17" +description = "ServiceNow HTTP client library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +ijson = ">=2.5.1,<3.0.0" +oauthlib = ">=3.1.0,<4.0.0" +python-magic = ">=0.4.15,<0.5.0" +pytz = ">=2019.3,<2020.0" +requests = ">=2.21.0,<3.0.0" +requests-oauthlib = ">=1.3.0,<2.0.0" +six = ">=1.13.0,<2.0.0" + +[[package]] +name = "python-crfsuite" +version = "0.9.7" +description = "Python binding for CRFsuite" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-magic" +version = "0.4.22" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pytz" +version = "2019.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "regex" +version = "2021.4.4" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.0" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sqlparse" +version = "0.4.1" +description = "A non-validating SQL parser." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "stevedore" +version = "3.3.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "structlog" +version = "20.2.0" +description = "Structured Logging for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] +docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tornado" +version = "6.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" +optional = false +python-versions = ">= 3.5" + +[[package]] +name = "tqdm" +version = "4.61.0" +description = "Fast, Extensible Progress Meter" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +telegram = ["requests"] + +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] + +[[package]] +name = "usaddress" +version = "0.5.10" +description = "Parse US addresses using conditional random fields" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +future = ">=0.14" +probableparsing = "*" +python-crfsuite = ">=0.7" + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "yamllint" +version = "1.26.1" +description = "A linter for YAML files." +category = "dev" +optional = false +python-versions = ">=3.5.*" + +[package.dependencies] +pathspec = ">=0.5.3" +pyyaml = "*" + +[[package]] +name = "zipp" +version = "3.4.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.6" +content-hash = "9e48593c0d66bc2466a225e78aa4399b66cb2b0bbf2b502f77c74e6c41ecc50f" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +asgiref = [ + {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, + {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, +] +astroid = [ + {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, + {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, +] +bandit = [ + {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, + {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +dataclasses = [ + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, +] +diffsync = [ + {file = "diffsync-1.3.0-py3-none-any.whl", hash = "sha256:c23bac1080ea7205272bacb4ae4659a23a96172a28024febe162a5559e178d0b"}, + {file = "diffsync-1.3.0.tar.gz", hash = "sha256:ab5499293e307f872056a757bea22e0c88ec91e88dfdba4d6bdc59832835b2af"}, +] +django = [ + {file = "Django-3.1.11-py3-none-any.whl", hash = "sha256:c79245c488411d1ae300b8f7a08ac18a496380204cf3035aff97ad917a8de999"}, + {file = "Django-3.1.11.tar.gz", hash = "sha256:9a0a2f3d34c53032578b54db7ec55929b87dda6fec27a06cc2587afbea1965e5"}, +] +django-debug-toolbar = [ + {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, + {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +gitpython = [ + {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, + {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +ijson = [ + {file = "ijson-2.6.1-cp27-cp27m-macosx_10_6_x86_64.whl", hash = "sha256:60393946d73792d5adeeaa25e82ff2f5bf19b17f6617a468743a4db4a07298a0"}, + {file = "ijson-2.6.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d320dc1c1c9adbe404668b0fed6bfa0ac8693911159564f4655a5f2059746993"}, + {file = "ijson-2.6.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a4cd7f8ecf035d0e23db1cc6d6036e6c563f31abacbceae88904bb8b7f88b1f6"}, + {file = "ijson-2.6.1-cp34-cp34m-macosx_10_6_x86_64.whl", hash = "sha256:9904bf55bc1f170353c32144861d8295f0bdc41034e5e6ae58cbf30610023ca6"}, + {file = "ijson-2.6.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7bac04b23691e6ab122d8f9ff06b26dbbb6df01babbf6bf8856ccad1c505278b"}, + {file = "ijson-2.6.1-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:ae9cc3ebbe8fa030b923b5dff912a61980edd03dc00b92f5c0223e44cbc51d9f"}, + {file = "ijson-2.6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8ce67b7d3435c3fc831d5c06f60b2d20a853b599cdf885478e575a3416fbf655"}, + {file = "ijson-2.6.1-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:a8b486bdf24e389947e588f4021498f6cc56cafdfaec1c78e9952e0f338aef23"}, + {file = "ijson-2.6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0042bb768fb890c177af923c0ead157cdc70c6dfa64827765c1a3676a879190"}, + {file = "ijson-2.6.1-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:26978c02314233c87bddad8800b7b9a56a052334f495e2bce93b282397c6931d"}, + {file = "ijson-2.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:25d4d159405f75a7443c1fe83b6d7be5a7da017b4aa9cc1bb5cda3feb74aaf32"}, + {file = "ijson-2.6.1.tar.gz", hash = "sha256:75ebc60b23abfb1c97f475ab5d07a5ed725ad4bd1f58513d8b258c21f02703d0"}, +] +importlib-metadata = [ + {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, + {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, +] +invoke = [ + {file = "invoke-1.5.0-py2-none-any.whl", hash = "sha256:da7c2d0be71be83ffd6337e078ef9643f41240024d6b2659e7b46e0b251e339f"}, + {file = "invoke-1.5.0-py3-none-any.whl", hash = "sha256:7e44d98a7dc00c91c79bac9e3007276965d2c96884b3c22077a9f04042bd6d90"}, + {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"}, +] +isort = [ + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, +] +jinja2 = [ + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, +] +joblib = [ + {file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"}, + {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, +] +livereload = [ + {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, +] +lunr = [ + {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, + {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, +] +markdown = [ + {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, + {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, +] +markdown-include = [ + {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mkdocs = [ + {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, + {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nltk = [ + {file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"}, + {file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"}, +] +oauthlib = [ + {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, + {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pbr = [ + {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, + {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, +] +probableparsing = [ + {file = "probableparsing-0.0.1-py2.py3-none-any.whl", hash = "sha256:509df25fdda4fd7c0b2a100f58cc971bd23daf26f3b3320aebf2616d2e10c69e"}, + {file = "probableparsing-0.0.1.tar.gz", hash = "sha256:8114bbf889e1f9456fe35946454c96e42a6ee2673a90d4f1f9c46a406f543767"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pydantic = [ + {file = "pydantic-1.7.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3c60039e84552442defbcb5d56711ef0e057028ca7bfc559374917408a88d84e"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6e7e314acb170e143c6f3912f93f2ec80a96aa2009ee681356b7ce20d57e5c62"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:8ef77cd17b73b5ba46788d040c0e820e49a2d80cfcd66fda3ba8be31094fd146"}, + {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:115d8aa6f257a1d469c66b6bfc7aaf04cd87c25095f24542065c68ebcb42fe63"}, + {file = "pydantic-1.7.4-cp36-cp36m-win_amd64.whl", hash = "sha256:66757d4e1eab69a3cfd3114480cc1d72b6dd847c4d30e676ae838c6740fdd146"}, + {file = "pydantic-1.7.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c92863263e4bd89e4f9cf1ab70d918170c51bd96305fe7b00853d80660acb26"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3b8154babf30a5e0fa3aa91f188356763749d9b30f7f211fafb247d4256d7877"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:80cc46378505f7ff202879dcffe4bfbf776c15675028f6e08d1d10bdfbb168ac"}, + {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:dda60d7878a5af2d8560c55c7c47a8908344aa78d32ec1c02d742ede09c534df"}, + {file = "pydantic-1.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4c1979d5cc3e14b35f0825caddea5a243dd6085e2a7539c006bc46997ef7a61a"}, + {file = "pydantic-1.7.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8857576600c32aa488f18d30833aa833b54a48e3bab3adb6de97e463af71f8f8"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f86d4da363badb39426a0ff494bf1d8510cd2f7274f460eee37bdbf2fd495ec"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3ea1256a9e782149381e8200119f3e2edea7cd6b123f1c79ab4bbefe4d9ba2c9"}, + {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e28455b42a0465a7bf2cde5eab530389226ce7dc779de28d17b8377245982b1e"}, + {file = "pydantic-1.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:47c5b1d44934375a3311891cabd450c150a31cf5c22e84aa172967bf186718be"}, + {file = "pydantic-1.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00250e5123dd0b123ff72be0e1b69140e0b0b9e404d15be3846b77c6f1b1e387"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d24aa3f7f791a023888976b600f2f389d3713e4f23b7a4c88217d3fce61cdffc"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2c44a9afd4c4c850885436a4209376857989aaf0853c7b118bb2e628d4b78c4e"}, + {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:e87edd753da0ca1d44e308a1b1034859ffeab1f4a4492276bff9e1c3230db4fe"}, + {file = "pydantic-1.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:a3026ee105b5360855e500b4abf1a1d0b034d88e75a2d0d66a4c35e60858e15b"}, + {file = "pydantic-1.7.4-py3-none-any.whl", hash = "sha256:a82385c6d5a77e3387e94612e3e34b77e13c39ff1295c26e3ba664e7b98073e2"}, + {file = "pydantic-1.7.4.tar.gz", hash = "sha256:0a1abcbd525fbb52da58c813d54c2ec706c31a91afdb75411a73dd1dec036595"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pylint = [ + {file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"}, + {file = "pylint-2.8.2.tar.gz", hash = "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217"}, +] +pylint-django = [ + {file = "pylint-django-2.4.4.tar.gz", hash = "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"}, + {file = "pylint_django-2.4.4-py3-none-any.whl", hash = "sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b"}, +] +pylint-plugin-utils = [ + {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, + {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, +] +pysnow = [ + {file = "pysnow-0.7.17-py2.py3-none-any.whl", hash = "sha256:c73b28a0d6b7a28518e7271b4af6cc94052985cec19e71bc7715e55c4d765862"}, + {file = "pysnow-0.7.17.tar.gz", hash = "sha256:9ca04d53b897999426854ab03577a9fdbefe925526373a2fd647ee551fe520ca"}, +] +python-crfsuite = [ + {file = "python-crfsuite-0.9.7.tar.gz", hash = "sha256:3b4538d2ce5007e4e42005818247bf43ade89ef08a66d158462e2f7c5d63cee7"}, + {file = "python_crfsuite-0.9.7-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:cd18b340c5a45ec200e8ce1167318dfc5d915ca9aad459dfa8675d014fd30650"}, + {file = "python_crfsuite-0.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d386d3a1e8d2065b4770c0dd06877ac28d7a94f61cd8447af3fa7a49551e98f9"}, + {file = "python_crfsuite-0.9.7-cp27-cp27m-win32.whl", hash = "sha256:2390c7cf62c72179b96c130048cec981173d3873ded532f739ba5ff770ed2d39"}, + {file = "python_crfsuite-0.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:bb57e551d86c83ec6a719c9884c571cb9a9b013a78fe0c317b0677c3c9542965"}, + {file = "python_crfsuite-0.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b46811138745d9d62ff7674bc7a14a9cc974c065dadfc6f78e0dc19832066ec2"}, + {file = "python_crfsuite-0.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4c9effa3cf7087cfecaa91ccada1ff9998b276bbde285700ef405345890253b1"}, + {file = "python_crfsuite-0.9.7-cp35-cp35m-win32.whl", hash = "sha256:b3da774cedf542202533b014347b86fbc25191356f0d5568f9784f8eb77e7ef6"}, + {file = "python_crfsuite-0.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:5ebb57783a0723d46d82d462fbfd6111e62e48533bfe1fbcd5ffb8dc1ba7a573"}, + {file = "python_crfsuite-0.9.7-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9934e684ff89ae97be52971c4c127329b1e1604ada9f903c7427a7062f256fc6"}, + {file = "python_crfsuite-0.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c56f34b50049de3127353214af45bc9b437fd6c23202b83abb0b8052d86a248b"}, + {file = "python_crfsuite-0.9.7-cp36-cp36m-win32.whl", hash = "sha256:8704a6b7c7c64c4aa158125c89e9e08377a0169e83c75094aa65833b771d3078"}, + {file = "python_crfsuite-0.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:1d2faa31771df2370bcf15855aa403416d14f088d3e81b19de857ea013a697b0"}, + {file = "python_crfsuite-0.9.7-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:d03ca82d34b45c6efa8f086eb05c7217e4a7fed34640e714775deaa08b61e6d2"}, + {file = "python_crfsuite-0.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0dc0149a62764e7d24d4f1a362f51b02e0283ac2b2469ce7f36666ece0b55855"}, + {file = "python_crfsuite-0.9.7-cp37-cp37m-win32.whl", hash = "sha256:b1568ab4c7a97f54b4d57f5b9795a4d6d841f7dc7923dd40414e34a93500cc42"}, + {file = "python_crfsuite-0.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:e905914a688138c29205a6752e768965ef3b0bfc46102b4a94316fd00dac7bc2"}, + {file = "python_crfsuite-0.9.7-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:397bac9cd4bae7a1b27d215c0119d33ff51c4ec5343d1f474867fd1a04c18a1d"}, + {file = "python_crfsuite-0.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:946ef3481c8dcd7c331123dd39b227cc52a386322967db78db650c58a6c972df"}, + {file = "python_crfsuite-0.9.7-cp38-cp38-win32.whl", hash = "sha256:df9edb37c90744c3aafd5d7dbf7c50fc486fe189e0e85a1deaf7af995ecac7b5"}, + {file = "python_crfsuite-0.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:caa980287a90fd8c659c6d936f5f4a5b28d0157ce530ad90a6430faed1cf147f"}, + {file = "python_crfsuite-0.9.7-py2.7-win-amd64.egg", hash = "sha256:a14959d27475f379711798e1cbdad79ebcab07976ea52d5b4862c36132ae16f5"}, + {file = "python_crfsuite-0.9.7-py2.7-win32.egg", hash = "sha256:9e8b03b02866c23e9618245757cf70cbdef18b9ce0893121c23ccd114fb78508"}, + {file = "python_crfsuite-0.9.7-py3.5-win-amd64.egg", hash = "sha256:09faa4425b9d8c128946c68c58c8efd5f28908ddf6b941af97475e2072f61495"}, + {file = "python_crfsuite-0.9.7-py3.5-win32.egg", hash = "sha256:4753c42cdd6c7f48ea745943f641c23d87a9547d22a07ea45903702cea1c7be2"}, + {file = "python_crfsuite-0.9.7-py3.6-win-amd64.egg", hash = "sha256:9aede38a4c93c90b9fa1b291c2e12521bcf718d6900beae0f933667f184c68ba"}, + {file = "python_crfsuite-0.9.7-py3.6-win32.egg", hash = "sha256:dfbfbfc298057e56532151910f042bb4b579502037d9403627a72cc51d572961"}, + {file = "python_crfsuite-0.9.7-py3.7-win-amd64.egg", hash = "sha256:ac25832a8ab55f3a0a91c863e7f4f270ccac9d34b2bf1e2ac457fc8e97c81ba2"}, + {file = "python_crfsuite-0.9.7-py3.7-win32.egg", hash = "sha256:468bcb736a98627df89708f631cfd0e0c5c7825b545ea1a1e91d7db2bbad88a6"}, + {file = "python_crfsuite-0.9.7-py3.8-win-amd64.egg", hash = "sha256:5cff06b51c16594ab4132d72a8b4b381ff4351a1825e388e120739c223ca849e"}, + {file = "python_crfsuite-0.9.7-py3.8-win32.egg", hash = "sha256:263f29c656fbb63d8d198d30ec9bca5b6fc7fab61fd20dd2f7cab795a613a85a"}, +] +python-magic = [ + {file = "python-magic-0.4.22.tar.gz", hash = "sha256:ca884349f2c92ce830e3f498c5b7c7051fe2942c3ee4332f65213b8ebff15a62"}, + {file = "python_magic-0.4.22-py2.py3-none-any.whl", hash = "sha256:8551e804c09a3398790bd9e392acb26554ae2609f29c72abb0b9dee9a5571eae"}, +] +pytz = [ + {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, + {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] +regex = [ + {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, + {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, + {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, + {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, + {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, + {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, + {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, + {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, + {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, + {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, + {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, + {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, + {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +requests-oauthlib = [ + {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, + {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, + {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, +] +sqlparse = [ + {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, + {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, +] +stevedore = [ + {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, + {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, +] +structlog = [ + {file = "structlog-20.2.0-py2.py3-none-any.whl", hash = "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd"}, + {file = "structlog-20.2.0.tar.gz", hash = "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tornado = [ + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, +] +tqdm = [ + {file = "tqdm-4.61.0-py2.py3-none-any.whl", hash = "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493"}, + {file = "tqdm-4.61.0.tar.gz", hash = "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042"}, +] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +urllib3 = [ + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, +] +usaddress = [ + {file = "usaddress-0.5.10-py2.py3-none-any.whl", hash = "sha256:4d66b11fdbec84f18d1bac8614fea2d1be1bb027e55849b5e2c1057bbca17437"}, + {file = "usaddress-0.5.10.tar.gz", hash = "sha256:1a8ebf62d0cce58d7d8286dde70373c530a9317b6fe1752a4197b75b7d0870e3"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] +yamllint = [ + {file = "yamllint-1.26.1.tar.gz", hash = "sha256:87d9462b3ed7e9dfa19caa177f7a77cd9888b3dc4044447d6ae0ab233bcd1324"}, +] +zipp = [ + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, +] diff --git a/nautobot-plugin-data-sync-servicenow/pyproject.toml b/nautobot-plugin-data-sync-servicenow/pyproject.toml new file mode 100644 index 000000000..f6aafc354 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/pyproject.toml @@ -0,0 +1,102 @@ +[tool.poetry] +name = "nautobot-data-sync-servicenow" +version = "0.1.0" +description = "Nautobot ServiceNow Data Synchronization" +authors = ["Network to Code, LLC "] + +license = "Apache-2.0" + +readme = "README.md" +homepage = "https://github.com/nautobot/nautobot-plugin-data-sync-servicenow" +repository = "https://github.com/nautobot/nautobot-plugin-data-sync-servicenow" +keywords = ["nautobot", "nautobot-plugin"] +include = [ + + "LICENSE", + + "README.md", +] +packages = [ + { include = "nautobot_data_sync_servicenow" }, +] + +[tool.poetry.dependencies] +python = "^3.6" +Jinja2 = "<3" +pysnow = "^0.7.17" +usaddress = "^0.5.10" +PyYAML = "^5.4.1" +diffsync = "^1.3.0" + +[tool.poetry.dev-dependencies] +invoke = "*" +black = "*" +django-debug-toolbar = "*" +yamllint = "*" +bandit = "*" +pylint = "*" +pylint-django = "*" +pydocstyle = "*" +flake8 = "*" +coverage = "*" +mkdocs = "*" +markdown-include = "*" + +[tool.poetry.plugins."nautobot_data_sync.sync_worker_classes"] +"ServiceNow export" = "nautobot_data_sync_servicenow.worker:ServiceNowExportDataSyncWorker" + +[tool.black] +line-length = 120 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + | settings.py # This is where you define files that should not be stylized by black + # the root of the project +) +''' + +[tool.pylint.master] +# Include the pylint_django plugin to avoid spurious warnings about Django patterns +load-plugins="pylint_django" + +[tool.pylint.basic] +# No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. +no-docstring-rgx="^(_|test_|Meta$)" + +[tool.pylint.messages_control] +# Line length is enforced by Black, so pylint doesn't need to check it. +# Pylint and Black disagree about how to format multi-line arrays; Black wins. +disable = """, + line-too-long, + bad-continuation, + """ + +[tool.pylint.miscellaneous] +# Don't flag TODO as a failure, let us commit with things that still need to be done in the code +notes = """, + FIXME, + XXX, + """ + +[build-system] +requires = ["poetry_core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +testpaths = [ + "tests" +] +addopts = "-vv --doctest-modules" diff --git a/nautobot-plugin-data-sync-servicenow/tasks.py b/nautobot-plugin-data-sync-servicenow/tasks.py new file mode 100644 index 000000000..af05528a7 --- /dev/null +++ b/nautobot-plugin-data-sync-servicenow/tasks.py @@ -0,0 +1,373 @@ +"""Tasks for use with Invoke. + +(c) 2020-2021 Network To Code +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from distutils.util import strtobool +from invoke import Collection, task as invoke_task +import os + + +def is_truthy(arg): + """Convert "truthy" strings into Booleans. + + Examples: + >>> is_truthy('yes') + True + Args: + arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, + f, false, off and 0. Raises ValueError if val is anything else. + """ + if isinstance(arg, bool): + return arg + return bool(strtobool(arg)) + + +# Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html +# Variables may be overwritten in invoke.yml or by the environment variables INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_xxx +namespace = Collection("nautobot_data_sync_servicenow") +namespace.configure( + { + "nautobot_data_sync_servicenow": { + "nautobot_ver": "develop-latest", + "project_name": "nautobot-data-sync-servicenow", + "python_ver": "3.6", + "local": False, + "compose_dir": os.path.join(os.path.dirname(__file__), "development"), + "compose_files": [ + "docker-compose.requirements.yml", + "docker-compose.base.yml", + "docker-compose.dev.yml", + "docker-compose.docs.yml", + ], + } + } +) + + +def task(function=None, *args, **kwargs): + """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" + + def task_wrapper(function=None): + """Wrapper around invoke.task to add the task to the namespace as well.""" + if args or kwargs: + task_func = invoke_task(*args, **kwargs)(function) + else: + task_func = invoke_task(function) + namespace.add_task(task_func) + return task_func + + if function: + # The decorator was called with no arguments + return task_wrapper(function) + # The decorator was called with arguments + return task_wrapper + + +def docker_compose(context, command, **kwargs): + """Helper function for running a specific docker-compose command with all appropriate parameters and environment. + + Args: + context (obj): Used to run specific commands + command (str): Command string to append to the "docker-compose ..." command, such as "build", "up", etc. + **kwargs: Passed through to the context.run() call. + """ + build_env = {"NAUTOBOT_VER": context.nautobot_data_sync_servicenow.nautobot_ver, "PYTHON_VER": context.nautobot_data_sync_servicenow.python_ver} + compose_command = f'docker-compose --project-name {context.nautobot_data_sync_servicenow.project_name} --project-directory "{context.nautobot_data_sync_servicenow.compose_dir}"' + for compose_file in context.nautobot_data_sync_servicenow.compose_files: + compose_file_path = os.path.join(context.nautobot_data_sync_servicenow.compose_dir, compose_file) + compose_command += f' -f "{compose_file_path}"' + compose_command += f" {command}" + print(f'Running docker-compose command "{command}"') + return context.run(compose_command, env=build_env, **kwargs) + + +def run_command(context, command, **kwargs): + """Wrapper to run a command locally or inside the nautobot container.""" + if is_truthy(context.nautobot_data_sync_servicenow.local): + context.run(command, **kwargs) + else: + # Check if netbox is running, no need to start another netbox container to run a command + docker_compose_status = "ps --services --filter status=running" + results = docker_compose(context, docker_compose_status, hide="out") + if "nautobot" in results.stdout: + compose_command = f"exec nautobot {command}" + else: + compose_command = f"run --entrypoint '{command}' nautobot" + + docker_compose(context, compose_command, pty=True) + + +# ------------------------------------------------------------------------------ +# BUILD +# ------------------------------------------------------------------------------ +@task( + help={ + "force_rm": "Always remove intermediate containers", + "cache": "Whether to use Docker's cache when building the image (defaults to enabled)", + } +) +def build(context, force_rm=False, cache=True): + """Build Nautobot docker image.""" + command = "build" + + if not cache: + command += " --no-cache" + if force_rm: + command += " --force-rm" + + print(f"Building Nautobot with Python {context.nautobot_data_sync_servicenow.python_ver}...") + docker_compose(context, command) + + +@task +def generate_packages(context): + """Generate all Python packages inside docker and copy the file locally under dist/.""" + command = "poetry build" + run_command(context, command) + + +# ------------------------------------------------------------------------------ +# START / STOP / DEBUG +# ------------------------------------------------------------------------------ +@task +def debug(context): + """Start Nautobot and its dependencies in debug mode.""" + print("Starting Nautobot in debug mode...") + docker_compose(context, "up") + + +@task +def start(context): + """Start Nautobot and its dependencies in detached mode.""" + print("Starting Nautobot in detached mode...") + docker_compose(context, "up --detach") + + +@task +def restart(context): + """Gracefully restart all containers.""" + print("Restarting Nautobot...") + docker_compose(context, "restart") + + +@task +def stop(context): + """Stop Nautobot and its dependencies.""" + print("Stopping Nautobot...") + docker_compose(context, "down") + + +@task +def destroy(context): + """Destroy all containers and volumes.""" + print("Destroying Nautobot...") + docker_compose(context, "down --volumes") + + +@task +def vscode(context): + """Launch Visual Studio Code with the appropriate Environment variables to run in a container.""" + command = "code nautobot.code-workspace" + + context.run(command) + + +# ------------------------------------------------------------------------------ +# ACTIONS +# ------------------------------------------------------------------------------ +@task +def nbshell(context): + """Launch an interactive nbshell session.""" + command = "nautobot-server nbshell" + run_command(context, command) + + +@task +def cli(context): + """Launch a bash shell inside the running Nautobot container.""" + run_command(context, "bash") + + +@task( + help={ + "user": "name of the superuser to create (default: admin)", + } +) +def createsuperuser(context, user="admin"): + """Create a new Nautobot superuser account (default: "admin"), will prompt for password.""" + command = f"nautobot-server createsuperuser --username {user}" + + run_command(context, command) + + +@task( + help={ + "name": "name of the migration to be created; if unspecified, will autogenerate a name", + } +) +def makemigrations(context, name=""): + """Perform makemigrations operation in Django.""" + command = "nautobot-server makemigrations nautobot_data_sync_servicenow" + + if name: + command += f" --name {name}" + + run_command(context, command) + + +@task +def migrate(context): + """Perform migrate operation in Django.""" + command = "nautobot-server migrate" + + run_command(context, command) + + +@task(help={}) +def post_upgrade(context): + """ + Performs Nautobot common post-upgrade operations using a single entrypoint. + + This will run the following management commands with default settings, in order: + + - migrate + - trace_paths + - collectstatic + - remove_stale_contenttypes + - clearsessions + - invalidate all + """ + command = "nautobot-server post_upgrade" + + run_command(context, command) + + +# ------------------------------------------------------------------------------ +# TESTS +# ------------------------------------------------------------------------------ +@task( + help={ + "autoformat": "Apply formatting recommendations automatically, rather than failing if formatting is incorrect.", + } +) +def black(context, autoformat=False): + """Check Python code style with Black.""" + if autoformat: + black_command = "black" + else: + black_command = "black --check --diff" + + command = f"{black_command} ." + + run_command(context, command) + + +@task +def flake8(context): + """Check for PEP8 compliance and other style issues.""" + command = "flake8 ." + run_command(context, command) + + +@task +def hadolint(context): + """Check Dockerfile for hadolint compliance and other style issues.""" + command = "hadolint development/Dockerfile" + run_command(context, command) + + +@task +def pylint(context): + """Run pylint code analysis.""" + command = 'pylint --init-hook "import nautobot; nautobot.setup()" --rcfile pyproject.toml nautobot_data_sync_servicenow' + run_command(context, command) + + +@task +def pydocstyle(context): + """Run pydocstyle to validate docstring formatting adheres to NTC defined standards.""" + # We exclude the /migrations/ directory since it is autogenerated code + command = "pydocstyle --config=.pydocstyle.ini ." + run_command(context, command) + + +@task +def bandit(context): + """Run bandit to validate basic static code security analysis.""" + command = "bandit --recursive . --configfile .bandit.yml" + run_command(context, command) + + +@task +def check_migrations(context): + """Check for missing migrations.""" + command = "nautobot-server --config=nautobot/core/tests/nautobot_config.py makemigrations --dry-run --check" + + run_command(context, command) + + +@task( + help={ + "keepdb": "save and re-use test database between test runs for faster re-testing.", + "label": "specify a directory or module to test instead of running all Nautobot tests", + "failfast": "fail as soon as a single test fails don't run the entire test suite", + "buffer": "Discard output from passing tests", + } +) +def unittest(context, keepdb=False, label="nautobot_data_sync_servicenow", failfast=False, buffer=True): + """Run Nautobot unit tests.""" + command = f"coverage run --module nautobot.core.cli test {label}" + + if keepdb: + command += " --keepdb" + if failfast: + command += " --failfast" + if buffer: + command += " --buffer" + run_command(context, command) + + +@task +def unittest_coverage(context): + """Report on code test coverage as measured by 'invoke unittest'.""" + command = "coverage report --skip-covered --include 'nautobot_data_sync_servicenow/*' --omit *migrations*" + + run_command(context, command) + + +@task( + help={ + "failfast": "fail as soon as a single test fails don't run the entire test suite", + } +) +def tests(context, failfast=False): + """Run all tests for this plugin.""" + # If we are not running locally, start the docker containers so we don't have to for each test + if not is_truthy(context.nautobot_data_sync_servicenow.local): + print("Starting Docker Containers...") + start(context) + # Sorted loosely from fastest to slowest + print("Running black...") + black(context) + print("Running flake8...") + flake8(context) + print("Running bandit...") + bandit(context) + print("Running pydocstyle...") + pydocstyle(context) + print("Running pylint...") + pylint(context) + print("Running unit tests...") + unittest(context, failfast=failfast) + print("All tests have passed!") + unittest_coverage(context) diff --git a/nautobot_data_sync/sync/__init__.py b/nautobot_data_sync/sync/__init__.py index 7a230c81d..463153ddb 100644 --- a/nautobot_data_sync/sync/__init__.py +++ b/nautobot_data_sync/sync/__init__.py @@ -85,7 +85,7 @@ def sync(sync_id, data): sync_worker = get_sync_worker_class(name=sync.job_result.name)(sync=sync, data=data) - sync_worker.execute(dry_run=sync.dry_run) + sync_worker.execute() except Exception as exc: sync.job_result.log( diff --git a/nautobot_data_sync/sync/base.py b/nautobot_data_sync/sync/base.py index 10662cea6..bab01c43b 100644 --- a/nautobot_data_sync/sync/base.py +++ b/nautobot_data_sync/sync/base.py @@ -14,6 +14,10 @@ class DataSyncWorker: """Semi-abstract base class to serve as a parent for all data sync worker implementations.""" + # Django: when passed this class or one of its subclasses as context for a template, + # DO NOT automatically instantiate it! + do_not_call_in_templates = True + def __init__(self, sync=None, data=None): """Instantiate a DataSyncWorker in preparation for executing the data sync. @@ -23,6 +27,7 @@ def __init__(self, sync=None, data=None): """ self.sync = sync self.data = data + self.dry_run = data.get("dry_run", True) if data else True class Meta: """Metaclass attributes of a DataSyncWorker. @@ -125,8 +130,8 @@ def sync_log( # Methods to be implemented by subclasses, below: # - def execute(self, dry_run=True): - """Perform a dry run or actual data synchronization.""" + def execute(self): + """Perform a dry run or actual data synchronization, depending on self.dry_run.""" def lookup_object(self, model_name, unique_id): """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. diff --git a/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html index 443a15504..c8a89bb3a 100644 --- a/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html +++ b/nautobot_data_sync/templates/nautobot_data_sync/sync_detail.html @@ -76,3 +76,7 @@

{% block title %}{{ object }}{% endblock %}

{% endblock %} + +{% block javascript %} + {% include 'extras/inc/jobresult_js.html' with result=object.job_result %} +{% endblock %} From 3f367f563c922ad72e0f5923fd55c1520a029fd9 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 28 May 2021 15:53:58 -0400 Subject: [PATCH 04/42] Renames --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .travis.yml | 6 +- README.md | 20 ++--- development/Dockerfile | 6 +- development/docker-compose.base.yml | 2 +- development/docker-compose.docs.yml | 2 +- development/nautobot_config.py | 6 +- docs/index.md | 2 +- invoke.example.yml | 4 +- mkdocs.yml | 8 +- .../nautobot_data_sync_servicenow/__init__.py | 24 ------ .../api/__init__.py | 1 - .../tests/__init__.py | 1 - .../nautobot_data_sync_servicenow/urls.py | 4 - .../.bandit.yml | 0 .../.dockerignore | 0 .../.flake8 | 0 .../.github/CODEOWNERS | 0 .../.github/ISSUE_TEMPLATE/bug_report.md | 4 +- .../.github/ISSUE_TEMPLATE/feature_request.md | 2 +- .../pull_request_template.md | 0 .../.gitignore | 0 .../.pydocstyle.ini | 0 .../.travis.yml | 0 .../.yamllint.yml | 0 .../FAQ.md | 0 .../LICENSE | 0 .../README.md | 20 ++--- .../development/Dockerfile | 0 .../development/creds.example.env | 0 .../development/dev.env | 0 .../development/docker-compose.base.yml | 2 +- .../development/docker-compose.dev.yml | 0 .../development/docker-compose.docs.yml | 2 +- .../docker-compose.requirements.yml | 0 .../development/nautobot_config.py | 2 +- .../docs/extra.css | 0 .../docs/index.md | 2 +- .../docs/requirements.txt | 0 .../invoke.example.yml | 4 +- .../mkdocs.yml | 8 +- .../nautobot_ssot_servicenow/__init__.py | 24 ++++++ .../nautobot_ssot_servicenow/api/__init__.py | 1 + .../data/mappings.yaml | 0 .../diffsync/__init__.py | 0 .../diffsync/adapter_nautobot.py | 0 .../diffsync/adapter_servicenow.py | 0 .../diffsync/models.py | 0 .../migrations/__init__.py | 0 .../nautobot_ssot_servicenow}/servicenow.py | 0 .../tests/__init__.py | 1 + .../tests/test_api.py | 4 +- .../tests/test_basic.py | 2 +- .../nautobot_ssot_servicenow/urls.py | 4 + .../nautobot_ssot_servicenow}/utilities.py | 2 +- .../nautobot_ssot_servicenow}/worker.py | 2 +- .../poetry.lock | 0 .../pyproject.toml | 16 ++-- .../tasks.py | 28 +++---- nautobot_data_sync/__init__.py | 24 ------ nautobot_data_sync/api/__init__.py | 1 - nautobot_data_sync/navigation.py | 18 ---- nautobot_data_sync/tests/__init__.py | 1 - nautobot_data_sync/urls.py | 22 ----- nautobot_ssot/__init__.py | 24 ++++++ nautobot_ssot/api/__init__.py | 1 + .../choices.py | 2 +- .../filters.py | 0 .../forms.py | 0 .../migrations/0001_initial.py | 6 +- .../migrations/__init__.py | 0 .../models.py | 2 +- nautobot_ssot/navigation.py | 18 ++++ .../sync/__init__.py | 8 +- .../sync/base.py | 4 +- .../sync/example.py | 4 +- .../tables.py | 6 +- .../templates/nautobot_ssot}/sync.html | 4 +- .../templates/nautobot_ssot}/sync_detail.html | 6 +- .../templates/nautobot_ssot}/sync_home.html | 2 +- nautobot_ssot/tests/__init__.py | 1 + .../tests/test_api.py | 4 +- .../tests/test_basic.py | 2 +- nautobot_ssot/urls.py | 22 +++++ .../views.py | 14 ++-- poetry.lock | 84 +++++++++---------- pyproject.toml | 16 ++-- tasks.py | 28 +++---- 89 files changed, 271 insertions(+), 277 deletions(-) delete mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py delete mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py delete mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py delete mode 100644 nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.bandit.yml (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.dockerignore (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.flake8 (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.github/CODEOWNERS (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.github/ISSUE_TEMPLATE/bug_report.md (84%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.github/ISSUE_TEMPLATE/feature_request.md (86%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.gitignore (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.pydocstyle.ini (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.travis.yml (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/.yamllint.yml (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/FAQ.md (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/LICENSE (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/README.md (84%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/Dockerfile (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/creds.example.env (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/dev.env (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/docker-compose.base.yml (87%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/docker-compose.dev.yml (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/docker-compose.docs.yml (71%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/docker-compose.requirements.yml (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/development/nautobot_config.py (99%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/docs/extra.css (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/docs/index.md (91%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/docs/requirements.txt (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/invoke.example.yml (73%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/mkdocs.yml (58%) create mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/__init__.py create mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/api/__init__.py rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/data/mappings.yaml (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/diffsync/__init__.py (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/diffsync/adapter_nautobot.py (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/diffsync/adapter_servicenow.py (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/diffsync/models.py (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/migrations/__init__.py (100%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/servicenow.py (100%) create mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/__init__.py rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/tests/test_api.py (89%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/tests/test_basic.py (88%) create mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/urls.py rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/utilities.py (96%) rename {nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow => nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow}/worker.py (97%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/poetry.lock (100%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/pyproject.toml (81%) rename {nautobot-plugin-data-sync-servicenow => nautobot-plugin-ssot-servicenow}/tasks.py (90%) delete mode 100644 nautobot_data_sync/__init__.py delete mode 100644 nautobot_data_sync/api/__init__.py delete mode 100644 nautobot_data_sync/navigation.py delete mode 100644 nautobot_data_sync/tests/__init__.py delete mode 100644 nautobot_data_sync/urls.py create mode 100644 nautobot_ssot/__init__.py create mode 100644 nautobot_ssot/api/__init__.py rename {nautobot_data_sync => nautobot_ssot}/choices.py (92%) rename {nautobot_data_sync => nautobot_ssot}/filters.py (100%) rename {nautobot_data_sync => nautobot_ssot}/forms.py (100%) rename {nautobot_data_sync => nautobot_ssot}/migrations/0001_initial.py (97%) rename {nautobot_data_sync => nautobot_ssot}/migrations/__init__.py (100%) rename {nautobot_data_sync => nautobot_ssot}/models.py (98%) create mode 100644 nautobot_ssot/navigation.py rename {nautobot_data_sync => nautobot_ssot}/sync/__init__.py (94%) rename {nautobot_data_sync => nautobot_ssot}/sync/base.py (98%) rename {nautobot_data_sync => nautobot_ssot}/sync/example.py (90%) rename {nautobot_data_sync => nautobot_ssot}/tables.py (94%) rename {nautobot_data_sync/templates/nautobot_data_sync => nautobot_ssot/templates/nautobot_ssot}/sync.html (84%) rename {nautobot_data_sync/templates/nautobot_data_sync => nautobot_ssot/templates/nautobot_ssot}/sync_detail.html (91%) rename {nautobot_data_sync/templates/nautobot_data_sync => nautobot_ssot/templates/nautobot_ssot}/sync_home.html (98%) create mode 100644 nautobot_ssot/tests/__init__.py rename {nautobot_data_sync => nautobot_ssot}/tests/test_api.py (91%) rename {nautobot_data_sync => nautobot_ssot}/tests/test_basic.py (89%) create mode 100644 nautobot_ssot/urls.py rename {nautobot_data_sync => nautobot_ssot}/views.py (89%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 54c69443a..7d78afc22 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,12 @@ --- name: 🐛 Bug Report -about: Report a reproducible bug in the current release of nautobot-data-sync +about: Report a reproducible bug in the current release of nautobot-ssot --- ### Environment * Python version: * Nautobot version: -* nautobot-data-sync version: +* nautobot-ssot version: ### Expected Behavior diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 0de801b3c..df23df317 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,7 +6,7 @@ about: Propose a new feature or enhancement ### Environment * Nautobot version: -* nautobot-data-sync version: +* nautobot-ssot version: +--> ### Use Case diff --git a/.travis.yml b/.travis.yml index 3a9a3c48f..3d8e5110e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: # Each version of Nautobot listed here must have a corresponding directory/configuration file # under development/nautobot_/configuration.py matrix: - - "INVOKE_NAUTOBOT-DATA-SYNC_NAUTOBOT_VER=1.0.1" + - "INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER=1.0.1" # Add your encrypted secret below, you can encrypt secret using "travis encrypt" # https://docs.travis-ci.com/user/environment-variables/#defining-encrypted-variables-in-travisyml # global: @@ -29,8 +29,8 @@ script: # If you want to test different versions, it may require updating the poetry definition for Nautobot # as that is where the version is controlled # - "poetry add nautobot=$NAUTOBOT_VER" - - "INVOKE_NAUTOBOT-DATA-SYNC_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke build --no-cache" - - "INVOKE_NAUTOBOT-DATA-SYNC_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke tests --failfast" + - "INVOKE_NAUTOBOT_SSOT_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke build --no-cache" + - "INVOKE_NAUTOBOT_SSOT_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke tests --failfast" # -------------------------------------------------------------------------- # Deploy # Uncomment the section below if you would like to publish a new release to pypi automatically diff --git a/README.md b/README.md index f69b1f45c..a4cb1b710 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nautobot Data Sync +# Nautobot Single Source of Truth (SSoT) A plugin for [Nautobot](https://github.com/nautobot/nautobot). @@ -7,25 +7,25 @@ A plugin for [Nautobot](https://github.com/nautobot/nautobot). The plugin is available as a Python package in pypi and can be installed with pip ```shell -pip install nautobot-data-sync +pip install nautobot-ssot ``` > The plugin is compatible with Nautobot 1.0.1 and higher -To ensure Nautobot Data Sync is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-data-sync` package: +To ensure Nautobot Single Source of Truth is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot` package: ```no-highlight -# echo nautobot-data-sync >> local_requirements.txt +# echo nautobot-ssot >> local_requirements.txt ``` Once installed, the plugin needs to be enabled in your `nautobot_configuration.py` ```python # In your configuration.py -PLUGINS = ["nautobot_data_sync"] +PLUGINS = ["nautobot_ssot"] # PLUGINS_CONFIG = { -# "nautobot_data_sync": { +# "nautobot_ssot": { # ADD YOUR SETTINGS HERE # } # } @@ -61,13 +61,13 @@ The development environment can be used in 2 ways. First, with a local poetry en The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: * `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest) -* `project_name`: the default docker compose project name (default: nautobot-data-sync) +* `project_name`: the default docker compose project name (default: nautobot-ssot) * `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) * `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) * `compose_dir`: the full path to a directory containing the project compose files * `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) -Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT-DATA-SYNC_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. +Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT_SSOT_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. #### Local Poetry Development Environment @@ -77,7 +77,7 @@ Using PyInvoke these configuration options can be overridden using [several meth ```shell --- -nautobot_data_sync: +nautobot_ssot: local: true compose_files: - "docker-compose.requirements.yml" @@ -121,7 +121,7 @@ Nautobot server can now be accessed at [http://localhost:8080](http://localhost: The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. -Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT-DATA-SYNC_PYTHON_VER` and `INVOKE_NAUTOBOT-DATA-SYNC_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` +Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT_SSOT_PYTHON_VER` and `INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` #### Docker dev environment diff --git a/development/Dockerfile b/development/Dockerfile index 892b294ef..c200f56bb 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -12,8 +12,8 @@ COPY poetry.lock pyproject.toml /source/ # and the project is copied in and installed after this step RUN poetry install --no-interaction --no-ansi --no-root -COPY nautobot-plugin-data-sync-servicenow/poetry.lock nautobot-plugin-data-sync-servicenow/pyproject.toml /source/nautobot-plugin-data-sync-servicenow/ -WORKDIR /source/nautobot-plugin-data-sync-servicenow +COPY nautobot-plugin-ssot-servicenow/poetry.lock nautobot-plugin-ssot-servicenow/pyproject.toml /source/nautobot-plugin-ssot-servicenow/ +WORKDIR /source/nautobot-plugin-ssot-servicenow RUN poetry install --no-interaction --no-ansi --no-root # Copy in the rest of the source code and install local Nautobot plugin @@ -21,7 +21,7 @@ WORKDIR /source COPY . /source RUN poetry install --no-interaction --no-ansi -WORKDIR /source/nautobot-plugin-data-sync-servicenow +WORKDIR /source/nautobot-plugin-ssot-servicenow RUN poetry install --no-interaction --no-ansi # Work around https://github.com/python-poetry/poetry/issues/3139 diff --git a/development/docker-compose.base.yml b/development/docker-compose.base.yml index 0a8034f72..1279543df 100644 --- a/development/docker-compose.base.yml +++ b/development/docker-compose.base.yml @@ -7,7 +7,7 @@ x-nautobot-build: &nautobot-build context: "../" dockerfile: "development/Dockerfile" x-nautobot-base: &nautobot-base - image: "nautobot-data-sync/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + image: "nautobot-ssot/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" env_file: - "dev.env" - "creds.env" diff --git a/development/docker-compose.docs.yml b/development/docker-compose.docs.yml index fe190b762..114d52b31 100644 --- a/development/docker-compose.docs.yml +++ b/development/docker-compose.docs.yml @@ -2,7 +2,7 @@ version: "3.4" services: docs: - image: "nautobot-data-sync/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + image: "nautobot-ssot/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" volumes: - "../docs:/source/docs:ro" diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 0d2fe0d23..c805799f0 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -238,11 +238,11 @@ def is_truthy(arg): "handlers": ["verbose_console" if DEBUG else "normal_console"], "level": LOG_LEVEL, }, - "nautobot_data_sync": { + "nautobot_ssot": { "handlers": ["verbose_console" if DEBUG else "normal_console"], "level": LOG_LEVEL, }, - "nautobot_data_sync_servicenow": { + "nautobot_ssot_servicenow": { "handlers": ["verbose_console" if DEBUG else "normal_console"], "level": LOG_LEVEL, }, @@ -289,7 +289,7 @@ def is_truthy(arg): PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50)) # Enable installed plugins. Add the name of each plugin to the list. -PLUGINS = ["nautobot_data_sync"] +PLUGINS = ["nautobot_ssot"] # Plugins configuration settings. These settings are used by various plugins that the user may have installed. # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. diff --git a/docs/index.md b/docs/index.md index 62a092d77..8c72ad4a6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# NautobotDataSync +# Nautobot Single Source of Truth TODO: Write plugin documentation, the outline here is provided as a guide and should be expanded upon. If more detail is required you are encouraged to expand on the table of contents (TOC) in `mkdocs.yml` to add additional pages. diff --git a/invoke.example.yml b/invoke.example.yml index 50b9d0530..ee3bb2ee8 100644 --- a/invoke.example.yml +++ b/invoke.example.yml @@ -1,6 +1,6 @@ --- -nautobot_data_sync: - project_name: "nautobot-data-sync" +nautobot_ssot: + project_name: "nautobot-ssot" nautobot_ver: "develop-latest" local: false python_ver: "3.6" diff --git a/mkdocs.yml b/mkdocs.yml index e0aff5b81..1e0796ae9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ --- dev_addr: "127.0.0.1:8001" -edit_uri: "edit/main/nautobot-plugin-data-sync/docs" -site_name: "NautobotDataSync Documentation" -site_url: "https://nautobot-plugin-data-sync.readthedocs.io/" -repo_url: "https://github.com/nautobot/nautobot-plugin-data-sync" +edit_uri: "edit/main/nautobot-plugin-ssot/docs" +site_name: "Nautobot Single Source of Truth Documentation" +site_url: "https://nautobot-plugin-ssot.readthedocs.io/" +repo_url: "https://github.com/nautobot/nautobot-plugin-ssot" python: install: - requirements: "docs/requirements.txt" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py deleted file mode 100644 index 74ca84c9d..000000000 --- a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Plugin declaration for nautobot_data_sync_servicenow.""" - -__version__ = "0.1.0" - -from nautobot.extras.plugins import PluginConfig - - -class NautobotDataSyncServicenowConfig(PluginConfig): - """Plugin configuration for the nautobot_data_sync_servicenow plugin.""" - - name = "nautobot_data_sync_servicenow" - verbose_name = "Nautobot ServiceNow Data Synchronization" - version = __version__ - author = "Network to Code, LLC" - description = "Nautobot ServiceNow Data Synchronization." - base_url = "data-sync-servicenow" - required_settings = [] - min_version = "1.0.0" - max_version = "1.9999" - default_settings = {} - caching_config = {} - - -config = NautobotDataSyncServicenowConfig # pylint:disable=invalid-name diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py deleted file mode 100644 index 93737379f..000000000 --- a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""REST API module for nautobot_data_sync_servicenow plugin.""" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py deleted file mode 100644 index b42edcee2..000000000 --- a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit tests for nautobot_data_sync_servicenow plugin.""" diff --git a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py b/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py deleted file mode 100644 index 539f60711..000000000 --- a/nautobot-plugin-data-sync-servicenow/nautobot_data_sync_servicenow/urls.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Django urlpatterns declaration for nautobot_data_sync_servicenow plugin.""" -# from django.urls import path - -urlpatterns = [] diff --git a/nautobot-plugin-data-sync-servicenow/.bandit.yml b/nautobot-plugin-ssot-servicenow/.bandit.yml similarity index 100% rename from nautobot-plugin-data-sync-servicenow/.bandit.yml rename to nautobot-plugin-ssot-servicenow/.bandit.yml diff --git a/nautobot-plugin-data-sync-servicenow/.dockerignore b/nautobot-plugin-ssot-servicenow/.dockerignore similarity index 100% rename from nautobot-plugin-data-sync-servicenow/.dockerignore rename to nautobot-plugin-ssot-servicenow/.dockerignore diff --git a/nautobot-plugin-data-sync-servicenow/.flake8 b/nautobot-plugin-ssot-servicenow/.flake8 similarity index 100% rename from nautobot-plugin-data-sync-servicenow/.flake8 rename to nautobot-plugin-ssot-servicenow/.flake8 diff --git a/nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS b/nautobot-plugin-ssot-servicenow/.github/CODEOWNERS similarity index 100% rename from nautobot-plugin-data-sync-servicenow/.github/CODEOWNERS rename to nautobot-plugin-ssot-servicenow/.github/CODEOWNERS diff --git a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md similarity index 84% rename from nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md rename to nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md index 9e9a2b088..4555e0542 100644 --- a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/bug_report.md +++ b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,12 @@ --- name: 🐛 Bug Report -about: Report a reproducible bug in the current release of nautobot-data-sync-servicenow +about: Report a reproducible bug in the current release of nautobot-ssot-servicenow --- ### Environment * Python version: * Nautobot version: -* nautobot-data-sync-servicenow version: +* nautobot-ssot-servicenow version: ### Expected Behavior diff --git a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md similarity index 86% rename from nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md rename to nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md index f2c12b1f4..7149223bb 100644 --- a/nautobot-plugin-data-sync-servicenow/.github/ISSUE_TEMPLATE/feature_request.md +++ b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,7 +6,7 @@ about: Propose a new feature or enhancement ### Environment * Nautobot version: -* nautobot-data-sync-servicenow version: +* nautobot-ssot-servicenow version: 1 Sync 1-->n SyncLogEntry 1-->1 ObjectChange +JobResult 1<->1 Sync 1-->n SyncLogEntry 1-->1 ObjectChange """ +from datetime import timedelta from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse +from django.utils.timezone import now from nautobot.core.models import BaseModel from nautobot.extras.models import ChangeLoggedModel, CustomFieldModel, JobResult, ObjectChange, RelationshipModel from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from .sync import get_data_source, get_data_target class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): @@ -34,14 +37,24 @@ class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): Essentially an extension of the JobResult model to add a few additional fields. """ + source = models.CharField(max_length=64, help_text="System data is read from") + target = models.CharField(max_length=64, help_text="System data is written to") + + start_time = models.DateTimeField(null=True) + # end_time is represented by the job_result.completed field + dry_run = models.BooleanField( default=False, help_text="Report what data would be synced but do not make any changes" ) diff = models.JSONField() + job_result = models.ForeignKey(to=JobResult, on_delete=models.PROTECT, blank=True, null=True) class Meta: - ordering = ["-created"] + ordering = ["start_time"] + + def __str__(self): + return f"{self.source} -> {self.target}, {self.start_time}" def get_absolute_url(self): return reverse("plugins:nautobot_ssot:sync", kwargs={"pk": self.pk}) @@ -68,6 +81,33 @@ def queryset(cls): ) ) + @property + def duration(self): + """Total execution time of this Sync.""" + if not self.start_time: + return timedelta() # zero + if not self.job_result.completed: + return now() - self.start_time + return self.job_result.completed - self.start_time + + def get_source_url(self): + """Get the absolute url of the source worker associated with this instance.""" + if self.source == "Nautobot": + return None + return reverse( + "plugins:nautobot_ssot:sync_add_source", + kwargs={"slug": get_data_source(name=self.source).slug}, + ) + + def get_target_url(self): + """Get the absolute url of the target worker associated with this instance.""" + if self.source == "Nautobot": + return None + return reverse( + "plugins:nautobot_ssot:sync_add_target", + kwargs={"slug": get_data_target(name=self.target).slug}, + ) + class SyncLogEntry(BaseModel): """Record of a single event during a data sync operation. diff --git a/nautobot_ssot/navigation.py b/nautobot_ssot/navigation.py index 737c26c58..290f215b8 100644 --- a/nautobot_ssot/navigation.py +++ b/nautobot_ssot/navigation.py @@ -5,6 +5,11 @@ menu_items = ( + PluginMenuItem( + link="plugins:nautobot_ssot:dashboard", + link_text="Dashboard", + permissions=["nautobot_ssot.view_sync"], + ), PluginMenuItem( link="plugins:nautobot_ssot:sync_list", link_text="History", diff --git a/nautobot_ssot/sync/__init__.py b/nautobot_ssot/sync/__init__.py index 541f5533e..25d95243c 100644 --- a/nautobot_ssot/sync/__init__.py +++ b/nautobot_ssot/sync/__init__.py @@ -12,7 +12,6 @@ from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices from nautobot_ssot.choices import SyncLogEntryActionChoices -from nautobot_ssot.models import Sync, SyncLogEntry logger = logging.getLogger("rq.worker") @@ -20,6 +19,8 @@ def log_to_log_entry(_logger, _log_method, event_dict): """Capture certain structlog messages from DiffSync into the Nautobot database.""" + # TODO rework this + from nautobot_ssot.models import SyncLogEntry if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): sync = event_dict["src"].sync sync_worker = event_dict["src"].sync_worker @@ -41,28 +42,49 @@ def log_to_log_entry(_logger, _log_method, event_dict): ) return event_dict +def get_data_sources(): + """Get a list of registered sync worker data sources.""" + return sorted( + [ + entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_sources") + ], + key=lambda worker: worker.name, + ) -def get_sync_worker_classes(): - return [ - entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.sync_worker_classes") - ] - - -def get_sync_worker_class(name=None, slug=None): - """Look up the specified sync worker class.""" - for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.sync_worker_classes"): - sync_worker_class = entrypoint.load() - if name and sync_worker_class.name == name: - return sync_worker_class - if slug and sync_worker_class.slug == slug: - return sync_worker_class - else: - raise KeyError(f'No registered sync worker "{name or slug}" found!') +def get_data_targets(): + """Get a list of registered sync worker data targets.""" + return sorted( + [ + entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_targets") + ], + key=lambda worker: worker.name, + ) +def get_data_source(name=None, slug=None): + """Look up the specified data source class.""" + for data_source in get_data_sources(): + if name and data_source.name != name: + continue + if slug and data_source.slug != slug: + continue + return data_source + raise KeyError(f'No data source "{name or slug}" found!') + +def get_data_target(name=None, slug=None): + """Look up the specified data target class.""" + for data_target in get_data_targets(): + if name and data_target.name != name: + continue + if slug and data_target.slug != slug: + continue + return data_target + raise KeyError(f'No data target "{name or slug}" found!') @job("default") def sync(sync_id, data): """Perform a requested sync.""" + # TODO rework this + from nautobot_ssot.models import Sync sync = Sync.objects.get(id=sync_id) sync.job_result.log( @@ -70,6 +92,8 @@ def sync(sync_id, data): ) sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) sync.job_result.save() + sync.start_time = timezone.now() + sync.save() try: structlog.configure( @@ -83,7 +107,10 @@ def sync(sync_id, data): cache_logger_on_first_use=True, ) - sync_worker = get_sync_worker_class(name=sync.job_result.name)(sync=sync, data=data) + if sync.source != "Nautobot": + sync_worker = get_data_source(name=sync.source)(sync=sync, data=data) + else: + sync_worker = get_data_target(name=sync.target)(sync=sync, data=data) sync_worker.execute() diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 01692270b..bf91df98e 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -1,11 +1,11 @@ """Data tables for Single Source of Truth (SSOT) views.""" - from django_tables2 import Column, DateTimeColumn, JSONColumn, LinkColumn, TemplateColumn from nautobot.utilities.tables import BaseTable, ToggleColumn from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices from .models import Sync, SyncLogEntry +from .sync import get_data_source, get_data_target ACTION_LOGS_LINK = """ @@ -36,12 +36,27 @@ MESSAGE_SPAN = """{% if record.message %}{{ record.message }}{% else %}—{% endif %}""" +class DashboardTable(BaseTable): + """Abbreviated version of SyncTable, for use with the dashboard.""" + + start_time = DateTimeColumn(linkify=True, short=True) + source = Column(linkify=lambda record: record.get_source_url()) + target = Column(linkify=lambda record: record.get_target_url()) + status = TemplateColumn(template_code="{% include 'extras/inc/job_label.html' with result=record.job_result %}") + dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") + + class Meta(BaseTable.Meta): + model = Sync + fields = ["source", "target", "start_time", "status", "dry_run"] + order_by = ["-start_time"] + + class SyncTable(BaseTable): """Table for listing Sync records.""" pk = ToggleColumn() - timestamp = DateTimeColumn(accessor="job_result.created", linkify=True, short=True, verbose_name="Timestamp") - name = Column(accessor="job_result.name") + start_time = DateTimeColumn(linkify=True, short=True) + duration = TemplateColumn(template_code="{% load shorter_timedelta %}{{ record.duration | shorter_timedelta }}") dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") status = TemplateColumn(template_code="{% include 'extras/inc/job_label.html' with result=record.job_result %}") @@ -82,14 +97,14 @@ class SyncTable(BaseTable): extra_context={"link_class": "num_errored", "status": SyncLogEntryStatusChoices.STATUS_ERROR}, ) - message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) - class Meta(BaseTable.Meta): model = Sync fields = ( "pk", - "timestamp", - "name", + "source", + "target", + "start_time", + "duration", "user", "status", "dry_run", @@ -100,12 +115,12 @@ class Meta(BaseTable.Meta): "num_succeeded", "num_failed", "num_errored", - "message", ) default_columns = ( "pk", - "timestamp", - "name", + "source", + "target", + "start_time", "status", "dry_run", "num_created", @@ -113,9 +128,8 @@ class Meta(BaseTable.Meta): "num_deleted", "num_failed", "num_errored", - "message", ) - order_by = ("-timestamp",) + order_by = ("-start_time",) ACTION_LABEL = """{{ record.action }}""" diff --git a/nautobot_ssot/templates/nautobot_ssot/dashboard.html b/nautobot_ssot/templates/nautobot_ssot/dashboard.html new file mode 100644 index 000000000..1a6898550 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/dashboard.html @@ -0,0 +1,69 @@ +{% extends 'base.html' %} +{% load buttons %} +{% load helpers %} +{% load static %} +{% load dashboard_helpers %} + +{% block content %} +

{% block title %}Single Source of Truth{% endblock %}

+ +
+
+
+
+ Data Sources +
+
+ {% if data_sources %} + {% for data_source in data_sources %} +
+ + {% dashboard_data data_source queryset "source" %} + +

+ + {{ data_source.name }} + +

+

{{ data_source.description }}

+
+ {% endfor %} + {% else %} +
— None found —
+ {% endif %} +
+
+
+
+ Data Targets +
+
+ {% if data_targets %} + {% for data_target in data_targets %} +
+ + {% dashboard_data data_target queryset "target" %} + +

+ + {{ data_target.name }} + +

+

{{ data_target.description }}

+
+ {% endfor %} + {% else %} +
— None found —
+ {% endif %} +
+
+
+
+ {% include table_template|default:'responsive_table.html' %} + +
+
+
+{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/history.html b/nautobot_ssot/templates/nautobot_ssot/history.html new file mode 100644 index 000000000..3506f36a6 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/history.html @@ -0,0 +1,14 @@ +{% extends 'generic/object_list.html' %} +{% load shorter_timedelta %} + +{% block header %} +
+
+ +
+
+{% endblock %} +{% block title %}SSoT Sync History{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_home.html b/nautobot_ssot/templates/nautobot_ssot/sync_home.html deleted file mode 100644 index 60d02a1bb..000000000 --- a/nautobot_ssot/templates/nautobot_ssot/sync_home.html +++ /dev/null @@ -1,115 +0,0 @@ -{% extends 'base.html' %} -{% load buttons %} -{% load helpers %} -{% load static %} - -{% block content %} -

{% block title %}Data Synchronization{% endblock %}

- -

Synchronization Operations

-
-
-
- - - - - - - - - - {% for sync_worker_class in sync_worker_classes %} - - - - - - {% endfor %} - -
NameDescription
- - - Execute - - - {{ sync_worker_class.name }} - - {{ sync_worker_class.description }} -
-
-
-
- -

Previous Data Syncs

-
- {% block buttons %}{% endblock %} - {% if request.user.is_authenticated and table_config_form %} - - {% endif %} -
-
-
-
- {% include 'inc/search_panel.html' %} - {% block sidebar %}{% endblock %} -
- {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %} - {% if permissions.change or permissions.delete %} -
- {% csrf_token %} - - {% if table.paginator.num_pages > 1 %} - - {% endif %} - {% include table_template|default:'responsive_table.html' %} -
- {% block bulk_buttons %}{% endblock %} - {% if bulk_edit_url and permissions.change %} - - {% endif %} - {% if bulk_delete_url and permissions.delete %} - - {% endif %} -
-
- {% else %} - {% include table_template|default:'responsive_table.html' %} - {% endif %} - {% endwith %} - {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} -
-
-
- {% table_config_form table table_name="ObjectTable" %} -{% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync.html b/nautobot_ssot/templates/nautobot_ssot/sync_run.html similarity index 100% rename from nautobot_ssot/templates/nautobot_ssot/sync.html rename to nautobot_ssot/templates/nautobot_ssot/sync_run.html diff --git a/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html b/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html new file mode 100644 index 000000000..77d5c01ce --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html @@ -0,0 +1,11 @@ +{% for status in statuses %} +   +{% endfor %} +{{ count }} diff --git a/nautobot_ssot/templatetags/__init__.py b/nautobot_ssot/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/templatetags/dashboard_helpers.py b/nautobot_ssot/templatetags/dashboard_helpers.py new file mode 100644 index 000000000..e44e60295 --- /dev/null +++ b/nautobot_ssot/templatetags/dashboard_helpers.py @@ -0,0 +1,21 @@ +import logging + +from django import template + + +logger = logging.getLogger(__name__) + + +register = template.Library() + + +@register.inclusion_tag("nautobot_ssot/templatetags/dashboard_data.html") +def dashboard_data(sync_worker_class, queryset, kind="source"): + if kind == "source": + records = queryset.filter(source=sync_worker_class.name).order_by("-start_time") + else: + records = queryset.filter(target=sync_worker_class.name).order_by("-start_time") + return { + "statuses": [record.job_result.status for record in records[:10]], + "count": records.count() + } diff --git a/nautobot_ssot/templatetags/shorter_timedelta.py b/nautobot_ssot/templatetags/shorter_timedelta.py new file mode 100644 index 000000000..cbf9f0455 --- /dev/null +++ b/nautobot_ssot/templatetags/shorter_timedelta.py @@ -0,0 +1,18 @@ +from django import template +from django.utils.html import format_html + + +register = template.Library() + + +@register.filter +def shorter_timedelta(timedelta): + """Render a timedelta as "HH:MM:SS.d" instead of "HH:MM:SS.mmmmmm". + + Note that we don't bother with rounding. + """ + if timedelta: + prefix, microseconds = str(timedelta).split(".", 1) + deciseconds = int(microseconds) // 100_000 + return f"{prefix}.{deciseconds}" + return format_html("—") diff --git a/nautobot_ssot/urls.py b/nautobot_ssot/urls.py index 5120a6343..bba2286bc 100644 --- a/nautobot_ssot/urls.py +++ b/nautobot_ssot/urls.py @@ -7,7 +7,19 @@ from . import models, views urlpatterns = [ - path("sync//", views.SyncCreateView.as_view(), name="sync_add"), + path("", views.DashboardView.as_view(), name="dashboard"), + path( + "sync/to//", + views.SyncCreateView.as_view(), + name="sync_add_target", + kwargs={"kind": "target"}, + ), + path( + "sync/from//", + views.SyncCreateView.as_view(), + name="sync_add_source", + kwargs={"kind": "source"}, + ), path("history/", views.SyncListView.as_view(), name="sync_list"), path("history/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), path("history//", views.SyncView.as_view(), name="sync"), diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 64713cd32..fb0484f8c 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -18,10 +18,40 @@ from .filters import SyncFilter, SyncLogEntryFilter from .forms import SyncFilterForm, SyncLogEntryFilterForm from .models import Sync, SyncLogEntry -from .sync import get_sync_worker_class, get_sync_worker_classes -from .tables import SyncTable, SyncLogEntryTable +from .sync import get_data_sources, get_data_targets, get_data_source, get_data_target +from .tables import DashboardTable, SyncTable, SyncLogEntryTable +class DashboardView(ObjectListView): + """Dashboard / overview of SSoT.""" + + queryset = Sync.queryset() + table = DashboardTable + action_buttons = [] + template_name = "nautobot_ssot/dashboard.html" + + def extra_context(self): + context = { + "queryset": self.queryset, + "data_sources": get_data_sources(), + "data_targets": get_data_targets(), + "source": {}, + "target": {}, + } + sync_ct = ContentType.objects.get_for_model(Sync) + for source in context["data_sources"]: + context["source"][source.name] = self.queryset.filter( + job_result__obj_type=sync_ct, + job_result__name=source.name, + ) + for target in context["data_targets"]: + context["target"][target.name] = self.queryset.filter( + job_result__obj_type=sync_ct, + job_result__name=target.name, + ) + + return context + class SyncListView(ObjectListView): """View for listing Sync records.""" @@ -30,10 +60,13 @@ class SyncListView(ObjectListView): filterset_form = SyncFilterForm table = SyncTable action_buttons = [] - template_name = "nautobot_ssot/sync_home.html" + template_name = "nautobot_ssot/history.html" def extra_context(self): - return {"sync_worker_classes": get_sync_worker_classes()} + return { + "data_sources": get_data_sources(), + "data_targets": get_data_targets(), + } class SyncCreateView(ObjectEditView): @@ -41,11 +74,14 @@ class SyncCreateView(ObjectEditView): queryset = Sync.objects.all() - def get(self, request, sync_worker_slug): + def get(self, request, slug, kind="source"): """Render a form for executing the given sync worker.""" try: - sync_worker_class = get_sync_worker_class(slug=sync_worker_slug) + if kind == "source": + sync_worker_class = get_data_source(slug=slug) + else: + sync_worker_class = get_data_target(slug=slug) except KeyError: raise Http404 @@ -53,17 +89,24 @@ def get(self, request, sync_worker_slug): return render( request, - "nautobot_ssot/sync.html", + "nautobot_ssot/sync_run.html", { "sync_worker_class": sync_worker_class, "form": form, }, ) - def post(self, request, sync_worker_slug): + def post(self, request, slug, kind="source"): """Enqueue the given sync worker for execution!""" try: - sync_worker_class = get_sync_worker_class(slug=sync_worker_slug) + if kind == "source": + sync_worker_class = get_data_source(slug=slug) + source = sync_worker_class.name + target = "Nautobot" + else: + sync_worker_class = get_data_target(slug=slug) + source = "Nautobot" + target = sync_worker_class.name except KeyError: raise Http404 @@ -75,7 +118,7 @@ def post(self, request, sync_worker_slug): if form.is_valid(): dry_run = form.cleaned_data.pop("dry_run") - sync = Sync.objects.create(dry_run=dry_run, diff={}) + sync = Sync.objects.create(source=source, target=target, dry_run=dry_run, diff={}) job_result = JobResult.objects.create( name=sync_worker_class.name, obj_type=ContentType.objects.get_for_model(sync), @@ -95,7 +138,7 @@ def post(self, request, sync_worker_slug): return render( request, - "nautobot_ssot/sync.html", + "nautobot_ssot/sync_run.html", { "sync_worker_class": sync_worker_class, "form": form, diff --git a/pyproject.toml b/pyproject.toml index 9fd1ecc00..8e356bed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,10 @@ coverage = "*" mkdocs = "*" markdown-include = "*" -[tool.poetry.plugins."nautobot_ssot.sync_worker_classes"] +[tool.poetry.plugins."nautobot_ssot.data_sources"] +"example" = "nautobot_ssot.sync.example:ExampleSyncWorker" + +[tool.poetry.plugins."nautobot_ssot.data_targets"] "example" = "nautobot_ssot.sync.example:ExampleSyncWorker" [tool.black] diff --git a/tasks.py b/tasks.py index d865f8fe0..2622046b1 100644 --- a/tasks.py +++ b/tasks.py @@ -15,6 +15,7 @@ from distutils.util import strtobool from invoke import Collection, task as invoke_task import os +import time def is_truthy(arg): @@ -252,6 +253,15 @@ def post_upgrade(context): run_command(context, command) +@task +def sql_import(context): + """Import nautobot_backup.dump into the database.""" + docker_compose(context, "up -d postgres") + time.sleep(2) + context.run(f"docker cp nautobot_backup.dump nautobot-ssot_postgres_1:/tmp/") + docker_compose(context, 'exec postgres sh -c "psql -h localhost -d nautobot -U nautbot < /tmp/nautobot_backup.dump"', pty=True) + + # ------------------------------------------------------------------------------ # TESTS # ------------------------------------------------------------------------------ From 7a47c4aedb73dc68163af39d4bebfbf941d364d6 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Jun 2021 16:46:28 -0400 Subject: [PATCH 06/42] Extract ServiceNow plugin to its own repository --- .gitignore | 1 + README.md | 3 +- development/Dockerfile | 12 +- nautobot-plugin-ssot-servicenow/.bandit.yml | 5 - nautobot-plugin-ssot-servicenow/.dockerignore | 20 - nautobot-plugin-ssot-servicenow/.flake8 | 4 - .../.github/CODEOWNERS | 2 - .../.github/ISSUE_TEMPLATE/bug_report.md | 25 - .../.github/ISSUE_TEMPLATE/feature_request.md | 22 - .../pull_request_template.md | 10 - nautobot-plugin-ssot-servicenow/.gitignore | 302 ---- .../.pydocstyle.ini | 11 - nautobot-plugin-ssot-servicenow/.travis.yml | 48 - nautobot-plugin-ssot-servicenow/.yamllint.yml | 10 - nautobot-plugin-ssot-servicenow/FAQ.md | 1 - nautobot-plugin-ssot-servicenow/LICENSE | 15 - nautobot-plugin-ssot-servicenow/README.md | 169 -- .../development/Dockerfile | 19 - .../development/creds.example.env | 10 - .../development/dev.env | 17 - .../development/docker-compose.base.yml | 32 - .../development/docker-compose.dev.yml | 16 - .../development/docker-compose.docs.yml | 11 - .../docker-compose.requirements.yml | 25 - .../development/nautobot_config.py | 320 ---- .../docs/extra.css | 19 - nautobot-plugin-ssot-servicenow/docs/index.md | 17 - .../docs/requirements.txt | 2 - .../invoke.example.yml | 11 - nautobot-plugin-ssot-servicenow/mkdocs.yml | 25 - .../nautobot_ssot_servicenow/__init__.py | 24 - .../nautobot_ssot_servicenow/api/__init__.py | 1 - .../data/mappings.yaml | 59 - .../diffsync/adapter_nautobot.py | 85 - .../diffsync/adapter_servicenow.py | 146 -- .../diffsync/models.py | 198 --- .../migrations/__init__.py | 0 .../nautobot_ssot_servicenow/servicenow.py | 165 -- .../tests/__init__.py | 1 - .../tests/test_api.py | 28 - .../tests/test_basic.py | 16 - .../nautobot_ssot_servicenow/urls.py | 4 - .../nautobot_ssot_servicenow/utilities.py | 93 -- .../nautobot_ssot_servicenow/worker.py | 66 - nautobot-plugin-ssot-servicenow/poetry.lock | 1362 ----------------- .../pyproject.toml | 100 -- nautobot-plugin-ssot-servicenow/tasks.py | 373 ----- .../__init__.py => packages/placeholder | 0 48 files changed, 6 insertions(+), 3899 deletions(-) delete mode 100644 nautobot-plugin-ssot-servicenow/.bandit.yml delete mode 100644 nautobot-plugin-ssot-servicenow/.dockerignore delete mode 100644 nautobot-plugin-ssot-servicenow/.flake8 delete mode 100644 nautobot-plugin-ssot-servicenow/.github/CODEOWNERS delete mode 100644 nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 nautobot-plugin-ssot-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md delete mode 100644 nautobot-plugin-ssot-servicenow/.gitignore delete mode 100644 nautobot-plugin-ssot-servicenow/.pydocstyle.ini delete mode 100644 nautobot-plugin-ssot-servicenow/.travis.yml delete mode 100644 nautobot-plugin-ssot-servicenow/.yamllint.yml delete mode 100644 nautobot-plugin-ssot-servicenow/FAQ.md delete mode 100644 nautobot-plugin-ssot-servicenow/LICENSE delete mode 100644 nautobot-plugin-ssot-servicenow/README.md delete mode 100644 nautobot-plugin-ssot-servicenow/development/Dockerfile delete mode 100644 nautobot-plugin-ssot-servicenow/development/creds.example.env delete mode 100644 nautobot-plugin-ssot-servicenow/development/dev.env delete mode 100644 nautobot-plugin-ssot-servicenow/development/docker-compose.base.yml delete mode 100644 nautobot-plugin-ssot-servicenow/development/docker-compose.dev.yml delete mode 100644 nautobot-plugin-ssot-servicenow/development/docker-compose.docs.yml delete mode 100644 nautobot-plugin-ssot-servicenow/development/docker-compose.requirements.yml delete mode 100644 nautobot-plugin-ssot-servicenow/development/nautobot_config.py delete mode 100644 nautobot-plugin-ssot-servicenow/docs/extra.css delete mode 100644 nautobot-plugin-ssot-servicenow/docs/index.md delete mode 100644 nautobot-plugin-ssot-servicenow/docs/requirements.txt delete mode 100644 nautobot-plugin-ssot-servicenow/invoke.example.yml delete mode 100644 nautobot-plugin-ssot-servicenow/mkdocs.yml delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/__init__.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/api/__init__.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/data/mappings.yaml delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_nautobot.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_servicenow.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/models.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/migrations/__init__.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/servicenow.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/__init__.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_api.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_basic.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/urls.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/utilities.py delete mode 100644 nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/worker.py delete mode 100644 nautobot-plugin-ssot-servicenow/poetry.lock delete mode 100644 nautobot-plugin-ssot-servicenow/pyproject.toml delete mode 100644 nautobot-plugin-ssot-servicenow/tasks.py rename nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/__init__.py => packages/placeholder (100%) diff --git a/.gitignore b/.gitignore index fd73de0ac..d253ecf79 100644 --- a/.gitignore +++ b/.gitignore @@ -298,6 +298,7 @@ fabric.properties # Rando creds.env nautobot_backup.dump +packages/ # Invoke overrides invoke.yml diff --git a/README.md b/README.md index a4cb1b710..aecd7dd5f 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,14 @@ This project is managed by [Python Poetry](https://python-poetry.org/) and has a 1. Install Poetry, see the [Poetry Documentation](https://python-poetry.org/docs/#installation) for your operating system. 2. Install Docker, see the [Docker documentation](https://docs.docker.com/get-docker/) for your operating system. +3. If you wish to use any data sources / data target libraries, add the appropriate `*.whl` files to the `packages/` directory - they will be automatically installed as part of any Docker build. (TODO change this eventually) Once you have Poetry and Docker installed you can run the following commands to install all other development dependencies in an isolated python virtual environment: ```shell poetry shell poetry install -invoke start +invoke build start ``` Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). diff --git a/development/Dockerfile b/development/Dockerfile index c200f56bb..710f73888 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -12,20 +12,14 @@ COPY poetry.lock pyproject.toml /source/ # and the project is copied in and installed after this step RUN poetry install --no-interaction --no-ansi --no-root -COPY nautobot-plugin-ssot-servicenow/poetry.lock nautobot-plugin-ssot-servicenow/pyproject.toml /source/nautobot-plugin-ssot-servicenow/ -WORKDIR /source/nautobot-plugin-ssot-servicenow -RUN poetry install --no-interaction --no-ansi --no-root +# Add worker plugin(s) if present +COPY packages/*.whl /tmp/packages/ +RUN pip install /tmp/packages/*.whl # Copy in the rest of the source code and install local Nautobot plugin WORKDIR /source COPY . /source RUN poetry install --no-interaction --no-ansi -WORKDIR /source/nautobot-plugin-ssot-servicenow -RUN poetry install --no-interaction --no-ansi - -# Work around https://github.com/python-poetry/poetry/issues/3139 -RUN pip install colorama - WORKDIR /source COPY development/nautobot_config.py /opt/nautobot/nautobot_config.py diff --git a/nautobot-plugin-ssot-servicenow/.bandit.yml b/nautobot-plugin-ssot-servicenow/.bandit.yml deleted file mode 100644 index 55c6741b1..000000000 --- a/nautobot-plugin-ssot-servicenow/.bandit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -skips: [] -# No need to check for security issues in the test scripts! -exclude_dirs: - - "./tests/" diff --git a/nautobot-plugin-ssot-servicenow/.dockerignore b/nautobot-plugin-ssot-servicenow/.dockerignore deleted file mode 100644 index ce354892c..000000000 --- a/nautobot-plugin-ssot-servicenow/.dockerignore +++ /dev/null @@ -1,20 +0,0 @@ -# Docker related -development/Dockerfile -development/docker-compose*.yml -development/*.env -*.env - -# Python -**/*.pyc -**/*.pyo - - -# Other -docs/_build -FAQ.md -.git/ -.gitignore -.github -tasks.py -LICENSE -**/*.log diff --git a/nautobot-plugin-ssot-servicenow/.flake8 b/nautobot-plugin-ssot-servicenow/.flake8 deleted file mode 100644 index e3ba27d5d..000000000 --- a/nautobot-plugin-ssot-servicenow/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -# E501: Line length is enforced by Black, so flake8 doesn't need to check it -# W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503 diff --git a/nautobot-plugin-ssot-servicenow/.github/CODEOWNERS b/nautobot-plugin-ssot-servicenow/.github/CODEOWNERS deleted file mode 100644 index 42b285c72..000000000 --- a/nautobot-plugin-ssot-servicenow/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Default owner(s) of all files in this repository -* @glennmatthews @jathanism @lampwins diff --git a/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 4555e0542..000000000 --- a/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: 🐛 Bug Report -about: Report a reproducible bug in the current release of nautobot-ssot-servicenow ---- - -### Environment -* Python version: -* Nautobot version: -* nautobot-ssot-servicenow version: - - -### Expected Behavior - - - -### Observed Behavior - - -### Steps to Reproduce -1. -2. -3. diff --git a/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md b/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7149223bb..000000000 --- a/nautobot-plugin-ssot-servicenow/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: ✨ Feature Request -about: Propose a new feature or enhancement - ---- - -### Environment -* Nautobot version: -* nautobot-ssot-servicenow version: - - -### Proposed Functionality - - -### Use Case - diff --git a/nautobot-plugin-ssot-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/nautobot-plugin-ssot-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index 2cf476470..000000000 --- a/nautobot-plugin-ssot-servicenow/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,10 +0,0 @@ -## New Pull Request - -Have you: -- [ ] Updated the README if necessary? -- [ ] Updated any configuration settings? -- [ ] Written a unit test? - -## Change Notes - -## Justification diff --git a/nautobot-plugin-ssot-servicenow/.gitignore b/nautobot-plugin-ssot-servicenow/.gitignore deleted file mode 100644 index c8dc4550a..000000000 --- a/nautobot-plugin-ssot-servicenow/.gitignore +++ /dev/null @@ -1,302 +0,0 @@ -# Ansible Retry Files -*.retry - -# Swap files -*.swp - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# Editor -.vscode/ - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### PyCharm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### PyCharm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -### vscode ### -.vscode/* -*.code-workspace - -# Rando -creds.env - -# Invoke overrides -invoke.yml diff --git a/nautobot-plugin-ssot-servicenow/.pydocstyle.ini b/nautobot-plugin-ssot-servicenow/.pydocstyle.ini deleted file mode 100644 index 541cc5106..000000000 --- a/nautobot-plugin-ssot-servicenow/.pydocstyle.ini +++ /dev/null @@ -1,11 +0,0 @@ -[pydocstyle] -convention = google -inherit = false -match = (?!__init__).*\.py -match-dir = (?!tests|migrations|development)[^\.].* -# D212 is enabled by default in google convention, and complains if we have a docstring like: -# """ -# My docstring is on the line after the opening quotes instead of on the same line as them. -# """ -# We've discussed and concluded that we consider this to be a valid style choice. -add_ignore = D212 \ No newline at end of file diff --git a/nautobot-plugin-ssot-servicenow/.travis.yml b/nautobot-plugin-ssot-servicenow/.travis.yml deleted file mode 100644 index 0f6c4c39c..000000000 --- a/nautobot-plugin-ssot-servicenow/.travis.yml +++ /dev/null @@ -1,48 +0,0 @@ ---- -language: "python" -python: - - "3.6" - - "3.7" - - "3.8" -env: - # Each version of Nautobot listed here must have a corresponding directory/configuration file - # under development/nautobot_/configuration.py - matrix: - - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_NAUTOBOT_VER=1.0.0" -# Add your encrypted secret below, you can encrypt secret using "travis encrypt" -# https://docs.travis-ci.com/user/environment-variables/#defining-encrypted-variables-in-travisyml -# global: -# secure: -services: - - "docker" -# -------------------------------------------------------------------------- -# Tests -# -------------------------------------------------------------------------- -before_script: - - "pip install invoke docker-compose" - - "curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > /tmp/get-poetry.py" - - "python /tmp/get-poetry.py -y --version 1.1.6" - - "source $HOME/.poetry/env" - - "cp development/creds.example.env development/creds.env" - -script: - # If you want to test different versions, it may require updating the poetry definition for Nautobot - # as that is where the version is controlled - # - "poetry add nautobot=$NAUTOBOT_VER" - - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke build --no-cache" - - "INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_PYTHON_VER=$TRAVIS_PYTHON_VERSION invoke tests --failfast" -# -------------------------------------------------------------------------- -# Deploy -# Uncomment the section below if you would like to publish a new release to pypi automatically -# when a new tag is created in master. You"ll also need to generate a dedicated API key for this project in pypi -# and encrypt the key with "travis encrypt PYPI_TOKEN= --add env.global --com" -# -------------------------------------------------------------------------- -# deploy: -# provider: script -# script: poetry config pypi-token.pypi $PYPI_TOKEN && poetry publish --build -# skip_cleanup: true -# on: -# tags: true -# branch: master -# condition: $NAUTOBOT_VER = master -# python: 3.7 diff --git a/nautobot-plugin-ssot-servicenow/.yamllint.yml b/nautobot-plugin-ssot-servicenow/.yamllint.yml deleted file mode 100644 index 58324ed12..000000000 --- a/nautobot-plugin-ssot-servicenow/.yamllint.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -extends: "default" -rules: - comments: "enable" - empty-values: "enable" - indentation: - indent-sequences: "consistent" - line-length: "disable" - quoted-strings: - quote-type: "double" diff --git a/nautobot-plugin-ssot-servicenow/FAQ.md b/nautobot-plugin-ssot-servicenow/FAQ.md deleted file mode 100644 index 318b08dc2..000000000 --- a/nautobot-plugin-ssot-servicenow/FAQ.md +++ /dev/null @@ -1 +0,0 @@ -# Frequently Asked Questions diff --git a/nautobot-plugin-ssot-servicenow/LICENSE b/nautobot-plugin-ssot-servicenow/LICENSE deleted file mode 100644 index 087f92f5c..000000000 --- a/nautobot-plugin-ssot-servicenow/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -Apache Software License 2.0 - -Copyright (c) 2021, Network to Code, LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/nautobot-plugin-ssot-servicenow/README.md b/nautobot-plugin-ssot-servicenow/README.md deleted file mode 100644 index a9eef5923..000000000 --- a/nautobot-plugin-ssot-servicenow/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Nautobot Single Source of Truth -- ServiceNow - -A plugin for [Nautobot](https://github.com/nautobot/nautobot). - -## Installation - -The plugin is available as a Python package in pypi and can be installed with pip - -```shell -pip install nautobot-ssot-servicenow -``` - -> The plugin is compatible with Nautobot 1.0.0 and higher - -To ensure nautobot-ssot-servicenow is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot-servicenow` package: - -```no-highlight -# echo nautobot-ssot-servicenow >> local_requirements.txt -``` - -Once installed, the plugin needs to be enabled in your `nautobot_configuration.py` - -```python -# In your configuration.py -PLUGINS = ["nautobot_ssot_servicenow"] - -# PLUGINS_CONFIG = { -# "nautobot_ssot_servicenow": { -# ADD YOUR SETTINGS HERE -# } -# } -``` - -The plugin behavior can be controlled with the following list of settings - -- TODO - -## Usage - -### API - -TODO - -## Contributing - -Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of Nautobot through TravisCI. - -The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI. - -The project is following Network to Code software development guideline and is leveraging: - -- Black, Pylint, Bandit and pydocstyle for Python linting and formatting. -- Django unit test to ensure the plugin is working properly. - -### Development Environment - -The development environment can be used in 2 ways. First, with a local poetry environment if you wish to develop outside of Docker. Second, inside of a docker container. - -#### Invoke tasks - -The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: - -* `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest) -* `project_name`: the default docker compose project name (default: nautobot-ssot-servicenow) -* `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) -* `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) -* `compose_dir`: the full path to a directory containing the project compose files -* `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) - -Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT_SSOT_SERVICENOW_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. - -#### Local Poetry Development Environment - -1. Copy `development/creds.example.env` to `development/creds.env` (This file will be ignored by git and docker) -2. Uncomment the `POSTGRES_HOST`, `REDIS_HOST`, and `NAUTOBOT_ROOT` variables in `development/creds.env` -3. Create an invoke.yml with the following contents at the root of the repo: - -```shell ---- -nautobot_ssot_servicenow: - local: true - compose_files: - - "docker-compose.requirements.yml" -``` - -3. Run the following commands: - -```shell -poetry shell -poetry install -export $(cat development/dev.env | xargs) -export $(cat development/creds.env | xargs) -``` - -4. You can now run nautobot-server commands as you would from the [Nautobot documentation](https://nautobot.readthedocs.io/en/latest/) for example to start the development server: - -```shell -nautobot-server runserver 0.0.0.0:8080 --insecure -``` - -Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). - -#### Docker Development Environment - -This project is managed by [Python Poetry](https://python-poetry.org/) and has a few requirements to setup your development environment: - -1. Install Poetry, see the [Poetry Documentation](https://python-poetry.org/docs/#installation) for your operating system. -2. Install Docker, see the [Docker documentation](https://docs.docker.com/get-docker/) for your operating system. - -Once you have Poetry and Docker installed you can run the following commands to install all other development dependencies in an isolated python virtual environment: - -```shell -poetry shell -poetry install -invoke start -``` - -Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). - -### CLI Helper Commands - -The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. - -Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT_SSOT_SERVICENOW_PYTHON_VER` and `INVOKE_NAUTOBOT_SSOT_SERVICENOW_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` - -#### Docker dev environment - -```no-highlight - build Build all docker images. - debug Start Nautobot and its dependencies in debug mode. - destroy Destroy all containers and volumes. - restart Restart Nautobot and its dependencies. - start Start Nautobot and its dependencies in detached mode. - stop Stop Nautobot and its dependencies. -``` - -#### Utility - -```no-highlight - cli Launch a bash shell inside the running Nautobot container. - create-user Create a new user in django (default: admin), will prompt for password. - makemigrations Run Make Migration in Django. - nbshell Launch a nbshell session. -``` - -#### Testing - -```no-highlight - bandit Run bandit to validate basic static code security analysis. - black Run black to check that Python files adhere to its style standards. - flake8 This will run flake8 for the specified name and Python version. - pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. - pylint Run pylint code analysis. - tests Run all tests for this plugin. - unittest Run Django unit tests for the plugin. -``` - -### Project Documentation - -Project documentation is generated by [mkdocs](https://www.mkdocs.org/) from the documentation located in the docs folder. You can configure [readthedocs.io](https://readthedocs.io/) to point at this folder in your repo. For development purposes a `docker-compose.docs.yml` is also included. A container hosting the docs will be started using the invoke commands on [http://localhost:8001](http://localhost:8001), as changes are saved the docs will be automatically reloaded. - -## Questions - -For any questions or comments, please check the [FAQ](FAQ.md) first and feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode). -Sign up [here](http://slack.networktocode.com/) - -## Screenshots - -TODO diff --git a/nautobot-plugin-ssot-servicenow/development/Dockerfile b/nautobot-plugin-ssot-servicenow/development/Dockerfile deleted file mode 100644 index c9d563798..000000000 --- a/nautobot-plugin-ssot-servicenow/development/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ - -ARG PYTHON_VER -ARG NAUTOBOT_VER -# FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} -FROM nniehoff/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} - -WORKDIR /source - -# Copy in only pyproject.toml/poetry.lock to help with caching this layer if no updates to dependencies -COPY poetry.lock pyproject.toml /source/ -# --no-root declares not to install the project package since we're wanting to take advantage of caching dependency installation -# and the project is copied in and installed after this step -RUN poetry install --no-interaction --no-ansi --no-root - -# Copy in the rest of the source code and install local Nautobot plugin -COPY . /source -RUN poetry install --no-interaction --no-ansi - -COPY development/nautobot_config.py /opt/nautobot/nautobot_config.py diff --git a/nautobot-plugin-ssot-servicenow/development/creds.example.env b/nautobot-plugin-ssot-servicenow/development/creds.example.env deleted file mode 100644 index 94bbf310e..000000000 --- a/nautobot-plugin-ssot-servicenow/development/creds.example.env +++ /dev/null @@ -1,10 +0,0 @@ -POSTGRES_PASSWORD=notverysecurepwd -REDIS_PASSWORD=notverysecurepwd -SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj -NAUTOBOT_CREATE_SUPERUSER=true -NAUTOBOT_SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 -NAUTOBOT_SUPERUSER_PASSWORD=admin -# POSTGRES_HOST=localhost -# REDIS_HOST=localhost -# NAUTOBOT_ROOT=./development - diff --git a/nautobot-plugin-ssot-servicenow/development/dev.env b/nautobot-plugin-ssot-servicenow/development/dev.env deleted file mode 100644 index eda6f4ec3..000000000 --- a/nautobot-plugin-ssot-servicenow/development/dev.env +++ /dev/null @@ -1,17 +0,0 @@ -ALLOWED_HOSTS=* -BANNER_TOP="Local" -CHANGELOG_RETENTION=0 -DEBUG=True -MAX_PAGE_SIZE=0 -METRICS_ENABLED=True -NAPALM_TIMEOUT=5 -NAUTOBOT_ROOT=/opt/nautobot -POSTGRES_DB=nautobot -POSTGRES_HOST=postgres -POSTGRES_USER=nautbot -REDIS_HOST=redis -REDIS_PORT=6379 -# REDIS_SSL=True -# Uncomment REDIS_SSL if using SSL -SUPERUSER_EMAIL=admin@example.com -SUPERUSER_NAME=admin diff --git a/nautobot-plugin-ssot-servicenow/development/docker-compose.base.yml b/nautobot-plugin-ssot-servicenow/development/docker-compose.base.yml deleted file mode 100644 index e182d671e..000000000 --- a/nautobot-plugin-ssot-servicenow/development/docker-compose.base.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -x-nautobot-build: &nautobot-build - build: - args: - NAUTOBOT_VER: "${NAUTOBOT_VER}" - PYTHON_VER: "${PYTHON_VER}" - context: "../" - dockerfile: "development/Dockerfile" -x-nautobot-base: &nautobot-base - image: "nautobot-ssot-servicenow/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" - env_file: - - "dev.env" - - "creds.env" - tty: true - -version: "3.4" -services: - nautobot: - ports: - - "8080:8080" - depends_on: - - "postgres" - - "redis" - <<: *nautobot-build - <<: *nautobot-base - worker: - entrypoint: "nautobot-server rqworker" - depends_on: - - "nautobot" - healthcheck: - disable: true - <<: *nautobot-base diff --git a/nautobot-plugin-ssot-servicenow/development/docker-compose.dev.yml b/nautobot-plugin-ssot-servicenow/development/docker-compose.dev.yml deleted file mode 100644 index 02e81f616..000000000 --- a/nautobot-plugin-ssot-servicenow/development/docker-compose.dev.yml +++ /dev/null @@ -1,16 +0,0 @@ -# We can't remove volumes in a compose override, for the test configuration using the final containers -# we don't want the volumes so this is the default override file to add the volumes in the dev case -# any override will need to include these volumes to use them. -# see: https://github.com/docker/compose/issues/3729 ---- -version: "3.4" -services: - nautobot: - command: "nautobot-server runserver 0.0.0.0:8080" - volumes: - - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - - "../:/source" - worker: - volumes: - - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - - "../:/source" diff --git a/nautobot-plugin-ssot-servicenow/development/docker-compose.docs.yml b/nautobot-plugin-ssot-servicenow/development/docker-compose.docs.yml deleted file mode 100644 index a09bafa15..000000000 --- a/nautobot-plugin-ssot-servicenow/development/docker-compose.docs.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -version: "3.4" -services: - docs: - image: "nautobot-ssot-servicenow/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" - entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" - volumes: - - "../docs:/source/docs:ro" - - "../mkdocs.yml:/source/mkdocs.yml:ro" - ports: - - "8001:8080" diff --git a/nautobot-plugin-ssot-servicenow/development/docker-compose.requirements.yml b/nautobot-plugin-ssot-servicenow/development/docker-compose.requirements.yml deleted file mode 100644 index 175cd297d..000000000 --- a/nautobot-plugin-ssot-servicenow/development/docker-compose.requirements.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -version: "3.4" -services: - postgres: - image: "postgres:13-alpine" - env_file: - - "dev.env" - - "creds.env" - volumes: - - "postgres_data:/var/lib/postgresql/data" - ports: - - "5432:5432" - redis: - image: "redis:6-alpine" - command: - - "sh" - - "-c" # this is to evaluate the $REDIS_PASSWORD from the env - - "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD" - env_file: - - "dev.env" - - "creds.env" - ports: - - "6379:6379" -volumes: - postgres_data: {} diff --git a/nautobot-plugin-ssot-servicenow/development/nautobot_config.py b/nautobot-plugin-ssot-servicenow/development/nautobot_config.py deleted file mode 100644 index 4be83c4b0..000000000 --- a/nautobot-plugin-ssot-servicenow/development/nautobot_config.py +++ /dev/null @@ -1,320 +0,0 @@ -######################### -# # -# Required settings # -# # -######################### - -import os -import sys - -from distutils.util import strtobool -from django.core.exceptions import ImproperlyConfigured -from nautobot.core import settings - -# Enforce required configuration parameters -for key in [ - "ALLOWED_HOSTS", - "POSTGRES_DB", - "POSTGRES_USER", - "POSTGRES_HOST", - "POSTGRES_PASSWORD", - "REDIS_HOST", - "REDIS_PASSWORD", - "SECRET_KEY", -]: - if not os.environ.get(key): - raise ImproperlyConfigured(f"Required environment variable {key} is missing.") - - -def is_truthy(arg): - """Convert "truthy" strings into Booleans. - - Examples: - >>> is_truthy('yes') - True - Args: - arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, - f, false, off and 0. Raises ValueError if val is anything else. - """ - if isinstance(arg, bool): - return arg - return bool(strtobool(arg)) - - -TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" - -# This is a list of valid fully-qualified domain names (FQDNs) for the Nautobot server. Nautobot will not permit write -# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. -# -# Example: ALLOWED_HOSTS = ['nautobot.example.com', 'nautobot.internal.local'] -ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(" ") - -# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: -# https://docs.djangoproject.com/en/stable/ref/settings/#databases -DATABASES = { - "default": { - "NAME": os.getenv("POSTGRES_DB", "nautobot"), # Database name - "USER": os.getenv("POSTGRES_USER", ""), # Database username - "PASSWORD": os.getenv("POSTGRES_PASSWORD", ""), # Datbase password - "HOST": os.getenv("POSTGRES_HOST", "localhost"), # Database server - "PORT": os.getenv("POSTGRES_PORT", ""), # Database port (leave blank for default) - "CONN_MAX_AGE": os.getenv("POSTGRES_TIMEOUT", 300), # Database timeout - "ENGINE": "django.db.backends.postgresql", # Database driver (Postgres only supported!) - } -} - -# Redis variables -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = os.getenv("REDIS_PORT", 6379) -REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") - -# Check for Redis SSL -REDIS_SCHEME = "redis" -REDIS_SSL = is_truthy(os.environ.get("REDIS_SSL", False)) -if REDIS_SSL: - REDIS_SCHEME = "rediss" - -# The django-redis cache is used to establish concurrent locks using Redis. The -# django-rq settings will use the same instance/database by default. -# -# This "default" server is now used by RQ_QUEUES. -# >> See: nautobot.core.settings.RQ_QUEUES -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": f"{REDIS_SCHEME}://{REDIS_HOST}:{REDIS_PORT}/0", - "TIMEOUT": 300, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - "PASSWORD": REDIS_PASSWORD, - }, - } -} - -# RQ_QUEUES is not set here because it just uses the default that gets imported -# up top via `from nautobot.core.settings import *`. - -# REDIS CACHEOPS -CACHEOPS_REDIS = f"{REDIS_SCHEME}://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/1" - -# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. -# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and -# symbols. Nautobot will not run without this defined. For more information, see -# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY -SECRET_KEY = os.environ["SECRET_KEY"] - - -######################### -# # -# Optional settings # -# # -######################### - -# Specify one or more name and email address tuples representing Nautobot administrators. These people will be notified of -# application errors (assuming correct email settings are provided). -ADMINS = [ - # ['John Doe', 'jdoe@example.com'], -] - -# URL schemes that are allowed within links in Nautobot -ALLOWED_URL_SCHEMES = ( - "file", - "ftp", - "ftps", - "http", - "https", - "irc", - "mailto", - "sftp", - "ssh", - "tel", - "telnet", - "tftp", - "vnc", - "xmpp", -) - -# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same -# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. -BANNER_TOP = os.environ.get("BANNER_TOP", "") -BANNER_BOTTOM = os.environ.get("BANNER_BOTTOM", "") - -# Text to include on the login page above the login form. HTML is allowed. -BANNER_LOGIN = os.environ.get("BANNER_LOGIN", "") - -# Cache timeout in seconds. Cannot be 0. Defaults to 900 (15 minutes). To disable caching, set CACHEOPS_ENABLED to False -CACHEOPS_DEFAULTS = {"timeout": 900} - -# Set to False to disable caching with cacheops. (Default: True) -CACHEOPS_ENABLED = True - -# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) -CHANGELOG_RETENTION = int(os.environ.get("CHANGELOG_RETENTION", 90)) - -# If True, all origins will be allowed. Other settings restricting allowed origins will be ignored. -# Defaults to False. Setting this to True can be dangerous, as it allows any website to make -# cross-origin requests to yours. Generally you'll want to restrict the list of allowed origins with -# CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGIN_REGEXES. -CORS_ORIGIN_ALLOW_ALL = is_truthy(os.environ.get("CORS_ORIGIN_ALLOW_ALL", False)) - -# A list of origins that are authorized to make cross-site HTTP requests. Defaults to []. -CORS_ALLOWED_ORIGINS = [ - # 'https://hostname.example.com', -] - -# A list of strings representing regexes that match Origins that are authorized to make cross-site -# HTTP requests. Defaults to []. -CORS_ALLOWED_ORIGIN_REGEXES = [ - # r'^(https?://)?(\w+\.)?example\.com$', -] - -# The file path where jobs will be stored. A trailing slash is not needed. Note that the default value of -# this setting is inside the invoking user's home directory. -# JOBS_ROOT = os.path.expanduser('~/.nautobot/jobs') - -# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal -# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging -# on a production system. -DEBUG = is_truthy(os.environ.get("DEBUG", False)) - -# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space -# within the global table (all prefixes and IP addresses not assigned to a VRF), set -# ENFORCE_GLOBAL_UNIQUE to True. -ENFORCE_GLOBAL_UNIQUE = is_truthy(os.environ.get("ENFORCE_GLOBAL_UNIQUE", False)) - -# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and -# by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. -EXEMPT_VIEW_PERMISSIONS = [ - # 'dcim.site', - # 'dcim.region', - # 'ipam.prefix', -] - -# HTTP proxies Nautobot should use when sending outbound HTTP requests (e.g. for webhooks). -# HTTP_PROXIES = { -# 'http': 'http://10.10.1.10:3128', -# 'https': 'http://10.10.1.10:1080', -# } - -# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing -# Nautobot from an internal IP. -INTERNAL_IPS = ("127.0.0.1", "::1") - -# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: -# https://docs.djangoproject.com/en/stable/topics/logging/ -LOGGING = {} - -# Setting this to True will display a "maintenance mode" banner at the top of every page. -MAINTENANCE_MODE = False - -# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. -# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request -# all objects by specifying "?limit=0". -MAX_PAGE_SIZE = int(os.environ.get("MAX_PAGE_SIZE", 1000)) - -# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that -# the default value of this setting is within the invoking user's home directory -# MEDIA_ROOT = os.path.expanduser('~/.nautobot/media') - -# By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the -# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: -# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' -# STORAGE_CONFIG = { -# 'AWS_ACCESS_KEY_ID': 'Key ID', -# 'AWS_SECRET_ACCESS_KEY': 'Secret', -# 'AWS_STORAGE_BUCKET_NAME': 'nautobot', -# 'AWS_S3_REGION_NAME': 'eu-west-1', -# } - -# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' -METRICS_ENABLED = False - -# Credentials that Nautobot will uses to authenticate to devices when connecting via NAPALM. -NAPALM_USERNAME = os.environ.get("NAPALM_USERNAME", "") -NAPALM_PASSWORD = os.environ.get("NAPALM_PASSWORD", "") - -# NAPALM timeout (in seconds). (Default: 30) -NAPALM_TIMEOUT = int(os.environ.get("NAPALM_TIMEOUT", 30)) - -# NAPALM optional arguments (see https://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must -# be provided as a dictionary. -NAPALM_ARGS = {} - -# Determine how many objects to display per page within a list. (Default: 50) -PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50)) - -# Enable installed plugins. Add the name of each plugin to the list. -PLUGINS = ["nautobot_ssot_servicenow"] - -# Plugins configuration settings. These settings are used by various plugins that the user may have installed. -# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. -# PLUGINS_CONFIG = { -# 'my_plugin': { -# 'foo': 'bar', -# 'buzz': 'bazz' -# } -# } - -# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to -# prefer IPv4 instead. -PREFER_IPV4 = is_truthy(os.environ.get("PREFER_IPV4", False)) - -# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. -RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22 -RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220 - -# Remote authentication support -REMOTE_AUTH_ENABLED = False -REMOTE_AUTH_BACKEND = "nautobot.core.authentication.RemoteUserBackend" -REMOTE_AUTH_HEADER = "HTTP_REMOTE_USER" -REMOTE_AUTH_AUTO_CREATE_USER = True -REMOTE_AUTH_DEFAULT_GROUPS = [] -REMOTE_AUTH_DEFAULT_PERMISSIONS = {} - -# This determines how often the GitHub API is called to check the latest release of Nautobot. Must be at least 1 hour. -RELEASE_CHECK_TIMEOUT = 24 * 3600 - -# This repository is used to check whether there is a new release of Nautobot available. Set to None to disable the -# version check or use the URL below to check for release in the official Nautobot repository. -RELEASE_CHECK_URL = None -# RELEASE_CHECK_URL = 'https://api.github.com/repos/nautobot/nautobot/releases' - -# Maximum execution time for background tasks, in seconds. -RQ_DEFAULT_TIMEOUT = 300 - -# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to -# re-authenticate. (Default: 1209600 [14 days]) -SESSION_COOKIE_AGE = 1209600 # 2 weeks, in seconds - - -# By default, Nautobot will store session data in the database. Alternatively, a file path can be specified here to use -# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only -# database access.) Note that the user as which Nautobot runs must have read and write permissions to this path. -SESSION_FILE_PATH = None - -# Configure SSO, for more information see docs/configuration/authentication/sso.md -SOCIAL_AUTH_ENABLED = False - -# Time zone (default: UTC) -TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") - -# Date/time formatting. See the following link for supported formats: -# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date -DATE_FORMAT = os.environ.get("DATE_FORMAT", "N j, Y") -SHORT_DATE_FORMAT = os.environ.get("SHORT_DATE_FORMAT", "Y-m-d") -TIME_FORMAT = os.environ.get("TIME_FORMAT", "g:i a") -SHORT_TIME_FORMAT = os.environ.get("SHORT_TIME_FORMAT", "H:i:s") -DATETIME_FORMAT = os.environ.get("DATETIME_FORMAT", "N j, Y g:i a") -SHORT_DATETIME_FORMAT = os.environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") - -# A list of strings designating all applications that are enabled in this Django installation. Each string should be a dotted Python path to an application configuration class (preferred), or a package containing an application. -# https://nautobot.readthedocs.io/en/latest/configuration/optional-settings/#extra-applications -EXTRA_INSTALLED_APPS = os.environ["EXTRA_INSTALLED_APPS"].split(",") if os.environ.get("EXTRA_INSTALLED_APPS") else [] - -# Django Debug Toolbar -DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING} - -if "debug_toolbar" not in EXTRA_INSTALLED_APPS: - EXTRA_INSTALLED_APPS.append("debug_toolbar") -if "debug_toolbar.middleware.DebugToolbarMiddleware" not in settings.MIDDLEWARE: - settings.MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/nautobot-plugin-ssot-servicenow/docs/extra.css b/nautobot-plugin-ssot-servicenow/docs/extra.css deleted file mode 100644 index 6a95f356a..000000000 --- a/nautobot-plugin-ssot-servicenow/docs/extra.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Images */ -img { - display: block; - margin-left: auto; - margin-right: auto; -} - -/* Tables */ -table { - margin-bottom: 24px; - width: 100%; -} -th { - background-color: #f0f0f0; - padding: 6px; -} -td { - padding: 6px; -} diff --git a/nautobot-plugin-ssot-servicenow/docs/index.md b/nautobot-plugin-ssot-servicenow/docs/index.md deleted file mode 100644 index 13ef358a4..000000000 --- a/nautobot-plugin-ssot-servicenow/docs/index.md +++ /dev/null @@ -1,17 +0,0 @@ -# Nautobot SSoT ServiceNow - -TODO: Write plugin documentation, the outline here is provided as a guide and should be expanded upon. If more detail is required you are encouraged to expand on the table of contents (TOC) in `mkdocs.yml` to add additional pages. - -## Description - -## Installation - -## Configuration - -## Usage - -## API - -## Views - -## Models diff --git a/nautobot-plugin-ssot-servicenow/docs/requirements.txt b/nautobot-plugin-ssot-servicenow/docs/requirements.txt deleted file mode 100644 index f354292fd..000000000 --- a/nautobot-plugin-ssot-servicenow/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs==1.1.2 -markdown-include==0.6.0 diff --git a/nautobot-plugin-ssot-servicenow/invoke.example.yml b/nautobot-plugin-ssot-servicenow/invoke.example.yml deleted file mode 100644 index 787906736..000000000 --- a/nautobot-plugin-ssot-servicenow/invoke.example.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -nautobot_ssot_servicenow: - project_name: "nautobot-ssot-servicenow" - nautobot_ver: "develop-latest" - local: false - python_ver: "3.6" - compose_dir: "development" - compose_files: - - "docker-compose.requirements.yml" - - "docker-compose.base.yml" - - "docker-compose.dev.yml" diff --git a/nautobot-plugin-ssot-servicenow/mkdocs.yml b/nautobot-plugin-ssot-servicenow/mkdocs.yml deleted file mode 100644 index f1cc9405f..000000000 --- a/nautobot-plugin-ssot-servicenow/mkdocs.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -dev_addr: "127.0.0.1:8001" -edit_uri: "edit/main/nautobot-plugin-ssot-servicenow/docs" -site_name: "Nautobot SSoT ServiceNow Documentation" -site_url: "https://nautobot-plugin-ssot-servicenow.readthedocs.io/" -repo_url: "https://github.com/nautobot/nautobot-plugin-ssot-servicenow" -python: - install: - - requirements: "docs/requirements.txt" -theme: - name: "readthedocs" - navigation_depth: 4 - hljs_languages: - - "django" - - "yaml" -extra_css: - - "extra.css" -markdown_extensions: - - "admonition" - - markdown_include.include: - headingOffset: 1 - - toc: - permalink: true -nav: - - Introduction: "index.md" diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/__init__.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/__init__.py deleted file mode 100644 index 2e6f0a724..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Plugin declaration for nautobot_ssot_servicenow.""" - -__version__ = "0.1.0" - -from nautobot.extras.plugins import PluginConfig - - -class NautobotSSOTServicenowConfig(PluginConfig): - """Plugin configuration for the nautobot_ssot_servicenow plugin.""" - - name = "nautobot_ssot_servicenow" - verbose_name = "Nautobot SSoT ServiceNow" - version = __version__ - author = "Network to Code, LLC" - description = "Nautobot SSoT ServiceNow." - base_url = "ssot-servicenow" - required_settings = [] - min_version = "1.0.0" - max_version = "1.9999" - default_settings = {} - caching_config = {} - - -config = NautobotSSOTServicenowConfig # pylint:disable=invalid-name diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/api/__init__.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/api/__init__.py deleted file mode 100644 index 0003ec683..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""REST API module for nautobot_ssot_servicenow plugin.""" diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/data/mappings.yaml b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/data/mappings.yaml deleted file mode 100644 index 4e59eaa8a..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/data/mappings.yaml +++ /dev/null @@ -1,59 +0,0 @@ ---- -- table: "cmn_location" - modelname: "location" - mappings: - - field: "name" - column: "name" - - field: "parent_location_name" - reference: - key: "parent" - table: "cmn_location" - column: "name" -- table: "cmdb_ci_ip_switch" - modelname: "device" - parent: - modelname: "location" - field: "location_name" - column: "location" - mappings: - - field: "name" - column: "name" - - field: "location_name" - reference: - key: "location" - table: "cmn_location" - column: "name" - - field: "model" - reference: - key: "model_id" - table: "cmdb_model" - column: "name" - - field: "platform" - reference: - key: "{{ app_prefix }}platform" - table: "{{ app_prefix }}platform" - column: "name" - - field: "role" - reference: - key: "{{ app_prefix }}device_role" - table: "{{ app_prefix }}device_role" - column: "name" - - field: "vendor" - reference: - key: "manufacturer" - table: "core_company" - column: "name" -- table: "cmdb_ci_network_adapter" - modelname: "interface" - parent: - modelname: "device" - field: "device_name" - column: "cmdb_ci" - mappings: - - field: "name" - column: "name" - - field: "device_name" - reference: - key: "cmdb_ci" - table: "cmdb_ci_ip_switch" - column: "name" diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_nautobot.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_nautobot.py deleted file mode 100644 index 9e784f896..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_nautobot.py +++ /dev/null @@ -1,85 +0,0 @@ -"""DiffSync adapter class for Nautobot as source-of-truth.""" - -from diffsync import DiffSync -from diffsync.exceptions import ObjectNotFound - -from nautobot.dcim.models import Device, Interface, Region, Site - -from . import models - - -class NautobotDiffSync(DiffSync): - """Nautobot adapter for DiffSync.""" - - location = models.Location - device = models.Device - interface = models.Interface - - top_level = [ - "location", - ] - - def load_regions(self, parent_location=None): - """Recursively add Nautobot Region objects as DiffSync Location models.""" - parent_pk = parent_location.pk if parent_location else None - for region_record in Region.objects.filter(parent=parent_pk): - location = self.location(diffsync=self, name=region_record.name, pk=region_record.pk) - if parent_location: - parent_location.contained_locations.append(location) - location.parent_location_name = parent_location.name - self.add(location) - self.load_regions(parent_location=location) - - def load_sites(self): - """Add Nautobot Site objects as DiffSync Location models.""" - for site_record in Site.objects.all(): - # A Site and a Region may share the same name; if so they become part of the same Location record. - try: - location = self.get(self.location, site_record.name) - except ObjectNotFound: - location = self.location(diffsync=self, name=site_record.name) - self.add(location) - if site_record.region: - if location.name != site_record.region.name: - region_location = self.get(self.location, site_record.region.name) - region_location.contained_locations.append(location) - location.parent_location_name = region_location.name - - def load_interface(self, interface_record, device_model): - """Import a single Nautobot Interface object as a DiffSync Interface model.""" - interface = self.interface( - diffsync=self, - name=interface_record.name, - device_name=device_model.name, - description=interface_record.description, - pk=interface_record.pk, - ) - self.add(interface) - device_model.add_child(interface) - - def load(self): - """Load data from Nautobot.""" - # Import all Nautobot Region records as Locations - self.load_regions(parent_location=None) - - # Import all Nautobot Site records as Locations - self.load_sites() - - for location in self.get_all(self.location): - for device_record in Device.objects.filter(site=location.pk): - device = self.device( - diffsync=self, - name=device_record.name, - platform=str(device_record.platform) if device_record.platform else None, - model=str(device_record.device_type), - role=str(device_record.device_role), - location_name=location.name, - vendor=str(device_record.device_type.manufacturer), - status=device_record.status, - pk=device_record.pk, - ) - self.add(device) - location.add_child(device) - - for interface_record in Interface.objects.filter(device=device_record): - self.load_interface(interface_record, device) diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_servicenow.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_servicenow.py deleted file mode 100644 index e559d95a1..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/adapter_servicenow.py +++ /dev/null @@ -1,146 +0,0 @@ -"""DiffSync adapter for ServiceNow.""" -import os - -from diffsync import DiffSync -from diffsync.exceptions import ObjectAlreadyExists -from jinja2 import Environment, FileSystemLoader -import yaml - -from . import models - - -class ServiceNowDiffSync(DiffSync): - """DiffSync adapter using pysnow to communicate with a ServiceNow server.""" - - location = models.Location - device = models.Device - interface = models.Interface - - top_level = [ - "location", - ] - - DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")) - - def __init__(self, *args, client=None, worker=None, **kwargs): - super().__init__(*args, **kwargs) - self.client = client - self.worker = worker - self.sys_ids = {} - self.mapping_data = [] - - def load(self): - """Load data via pysnow.""" - self.mapping_data = self.load_yaml_datafile("mappings.yaml", {"app_prefix": self.client.app_prefix}) - - for entry in self.mapping_data: - self.load_table(**entry) - - @classmethod - def load_yaml_datafile(cls, filename, config): - """Get the contents of the given YAML data file. - - Args: - filename (str): Filename within the 'data' directory. - config (dict): Data for Jinja2 templating. - """ - file_path = os.path.join(cls.DATA_DIR, filename) - if not os.path.isfile(file_path): - raise RuntimeError(f"No data file found at {file_path}") - env = Environment(loader=FileSystemLoader(cls.DATA_DIR), autoescape=True) - template = env.get_template(filename) - populated = template.render(config) - return yaml.safe_load(populated) - - def load_table(self, modelname, table, mappings, **kwargs): - """Load data from the ServiceNow "table" into the DiffSync model. - - Args: - modelname (str): DiffSync model class identifier, such as "location" or "device". - table (str): ServiceNow table name, such as "cmdb_ci_ip_switch" - mappings (list): List of dicts, each stating how to populate a field in the model. - **kwargs: Optional arguments, all of which default to False if unset: - - - parent (dict): Dict of {"modelname": ..., "field": ...} used to link table records back to their parents - """ - model_cls = getattr(self, modelname) - self.worker.job_log(f"Loading table {table} into {modelname} instances...") - - if "parent" not in kwargs: - # Load the entire table - for record in self.client.all_table_entries(table): - self.load_record(table, record, model_cls, mappings, **kwargs) - else: - # Load items per parent object that we know/care about - # This is necessary because, for example, the cmdb_ci_network_adapter table contains network interfaces - # for ALL types of devices (servers, switches, firewalls, etc.) but we only have switches as parent objects - for parent in self.get_all(kwargs["parent"]["modelname"]): - self.worker.job_log(f"Loading children of {parent}") - for record in self.client.all_table_entries(table, {kwargs["parent"]["column"]: parent.sys_id}): - self.load_record(table, record, model_cls, mappings, **kwargs) - - def load_record(self, table, record, model_cls, mappings, **kwargs): - """Helper method to load_table().""" - self.sys_ids.setdefault(table, {})[record["sys_id"]] = record - - ids_attrs = self.map_record_to_attrs(record, mappings) - model = model_cls(**ids_attrs) - modelname = model.get_type() - - try: - self.add(model) - self.worker.job_log(f"Loaded {modelname} {model.get_unique_id()}") - except ObjectAlreadyExists: - # TODO: the baseline data in ServiceNow has a number of duplicate Location entries. For now, continue - self.worker.job_log(f"Duplicate object encountered for {modelname} {model.get_unique_id()}") - - if "parent" in kwargs: - parent_uid = getattr(model, kwargs["parent"]["field"]) - if parent_uid is None: - self.worker.job_log( - f"Model {modelname} {model.get_unique_id} does not have a parent uid value in field {kwargs['parent']['field']}" - ) - else: - parent_model = self.get(kwargs["parent"]["modelname"], parent_uid) - parent_model.add_child(model) - self.worker.job_log( - f"Recorded {modelname} {model.get_unique_id()} as a child of {parent_model.get_type()} {parent_model.get_unique_id()}" - ) - - def map_record_to_attrs(self, record, mappings): # TODO pylint: disable=too-many-branches - """Helper method to load_table().""" - attrs = {"sys_id": record["sys_id"]} - for mapping in mappings: - value = None - if "column" in mapping: - value = record[mapping["column"]] - elif "reference" in mapping: - # Reference by sys_id to a field in a record in another table - table = mapping["reference"]["table"] - if "key" in mapping["reference"]: - key = mapping["reference"]["key"] - if key not in record: - self.worker.job_log(f"Key {key} is not present in record {record}") - else: - sys_id = record[key] - else: - raise NotImplementedError - - if sys_id: - if sys_id not in self.sys_ids.get(table, {}): - referenced_record = self.client.get_by_sys_id(table, sys_id) - if referenced_record is None: - self.worker.job_log( - f"Record references sys_id {sys_id}, but that was not found in table {table}" - ) - else: - self.sys_ids.setdefault(table, {})[sys_id] = referenced_record - - if sys_id in self.sys_ids.get(table, {}): - value = self.sys_ids[table][sys_id][mapping["reference"]["column"]] - else: - raise NotImplementedError - - attrs[mapping["field"]] = value - - return attrs diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/models.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/models.py deleted file mode 100644 index 6b3ada83c..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/models.py +++ /dev/null @@ -1,198 +0,0 @@ -"""DiffSyncModel subclasses for Nautobot-to-ServiceNow data sync.""" -from typing import List, Optional -import uuid - -from diffsync import DiffSyncModel -import pysnow - - -class ServiceNowCRUDMixin: - """Mixin class for all ServiceNow models, to support CRUD operations based on mappings.yaml.""" - - def map_data_to_sn_record(self, data, mapping_entry, existing_record=None): - """Map create/update data from DiffSync to a corresponding ServiceNow data record.""" - record = existing_record or {} - for mapping in mapping_entry.get("mappings", []): - if mapping["field"] not in data: - continue - value = data[mapping["field"]] - if "column" in mapping: - record[mapping["column"]] = value - elif "reference" in mapping: - tablename = mapping["reference"]["table"] - target = None - if "column" in mapping["reference"]: - if value is not None: - target = self.diffsync.client.get_by_query(tablename, {mapping["reference"]["column"]: value}) - if target is None: - self.diffsync.worker.job_log(f"Unable to find reference target in {tablename}") - else: - raise NotImplementedError - - sys_id = target["sys_id"] if target else None - record[mapping["reference"]["key"]] = sys_id - else: - raise NotImplementedError - - self.diffsync.worker.job_log(f"Mapped data {data} to record {record}") - return record - - @classmethod - def create(cls, diffsync, ids, attrs): - """Create a new instance, data-driven by mappings.""" - entry = None - for item in diffsync.mapping_data: - if item["modelname"] == cls.get_type(): - entry = item - break - - if not entry: - raise RuntimeError(f"Did not find a mapping entry for {cls.get_type()}!") - - model = super().create(diffsync, ids=ids, attrs=attrs) - - sn_resource = diffsync.client.resource(api_path=f"/table/{entry['table']}") - sn_record = model.map_data_to_sn_record(data=dict(**ids, **attrs), mapping_entry=entry) - sn_resource.create(payload=sn_record) - - return model - - def update(self, attrs): - """Update an existing instance, data-driven by mappings.""" - entry = None - for item in self.diffsync.mapping_data: - if item["modelname"] == self.get_type(): - entry = item - break - - if not entry: - raise RuntimeError("Did not find a mapping entry for {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: - record = sn_resource.get(query=query).one() - except pysnow.exceptions.MultipleResults: - self.diffsync.worker.job_log( - f"Unsure which record to update, as query {query} matched more than one item in table {entry['table']}" - ) - return None - - sn_record = self.map_data_to_sn_record(data=attrs, mapping_entry=entry, existing_record=record) - sn_resource.update(query=query, payload=sn_record) - - super().update(attrs) - return self - - # TODO delete() method - -class Location(ServiceNowCRUDMixin, DiffSyncModel): - """ServiceNow Location model.""" - - _modelname = "location" - _identifiers = ("name",) - _attributes = ("parent_location_name",) - _children = { - "device": "devices", - } - - name: str - - parent_location_name: Optional[str] - contained_locations: List["Location"] = list() - - devices: List["Device"] = list() - - sys_id: Optional[str] = None - pk: Optional[uuid.UUID] = None - - -class Device(ServiceNowCRUDMixin, DiffSyncModel): - """ServiceNow Device model.""" - - _modelname = "device" - _identifiers = ("name",) - # For now we do not store more of the device fields in ServiceNow: - # platform, model, role, vendor - # ...as we would need to sync these data models to ServiceNow as well, and we don't do that yet. - _attributes = ( - "location_name", - ) - _children = { - "interface": "interfaces", - } - - name: str - - location_name: Optional[str] - model: Optional[str] - platform: Optional[str] - role: Optional[str] - vendor: Optional[str] - - interfaces: List["Interface"] = list() - - sys_id: Optional[str] = None - pk: Optional[uuid.UUID] = None - - -class Interface(ServiceNowCRUDMixin, DiffSyncModel): - """ServiceNow Interface model.""" - - _modelname = "interface" - _identifiers = ( - "device_name", - "name", - ) - _shortname = ("name",) - # ServiceNow currently stores very little data about interfaces that we are interested in - _attributes = () - - _children = {"ip_address": "ip_addresses"} - - name: str - device_name: str - - access_vlan: Optional[int] - active: Optional[bool] - allowed_vlans: List[str] = list() - description: Optional[str] - is_virtual: Optional[bool] - is_lag: Optional[bool] - is_lag_member: Optional[bool] - lag_members: List[str] = list() - mode: Optional[str] # TRUNK, ACCESS, L3, NONE - mtu: Optional[int] - parent: Optional[str] - speed: Optional[int] - switchport_mode: Optional[str] - type: Optional[str] - - ip_addresses: List["IPAddress"] = list() - - sys_id: Optional[str] = None - pk: Optional[uuid.UUID] = None - - -class IPAddress(ServiceNowCRUDMixin, DiffSyncModel): - """An IPv4 or IPv6 address.""" - - _modelname = "ip_address" - _identifiers = ("address",) - _attributes = ( - "device_name", - "interface_name", - ) - - address: str # TODO: change to netaddr.IPAddress? - - device_name: Optional[str] - interface_name: Optional[str] - - sys_id: Optional[str] = None - pk: Optional[uuid.UUID] = None - - -Location.update_forward_refs() -Device.update_forward_refs() -Interface.update_forward_refs() diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/migrations/__init__.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/servicenow.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/servicenow.py deleted file mode 100644 index bf91be9dc..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/servicenow.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Interactions with ServiceNow APIs.""" -import logging -import os - -from pysnow import Client -import requests - - -logger = logging.getLogger(__name__) - - -def python_value_to_string(value): - """The ServiceNow REST API represents everything as a string, so map Python values to their API representation.""" - if value is None: - value = "" - elif isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - return value - - -class ServiceNowClient(Client): - """Extend the pysnow Client with additional use-case-specific functionality.""" - - def __init__(self, instance="", username="", password="", app_prefix="", worker=None): - """Create a ServiceNowClient with the appropriate environment parameters.""" - super().__init__(instance=instance, user=username, password=password) - - self.worker = worker - self.app_prefix = app_prefix - - # When getting records from ServiceNow, for reference fields, only return the sys_id value of the reference, - # rather than returning a dict of {"link": "https://.servicenow.com/...", "value": } - # We don't need the link for our purposes, and including it makes it harder to preserve idempotence. - self.parameters.exclude_reference_link = True - - def all_table_entries(self, table, query=None): - """Iterator over all records in a given table.""" - if not query: - query = {} - logger.debug("Getting all entries in table %s matching query %s", table, query) - yield from self.resource(api_path=f"/table/{table}").get(query=query, stream=True).all() - - def get_by_sys_id(self, table, sys_id): - """Get a record with a given sys_id from a given table.""" - return self.get_by_query(table, {"sys_id": sys_id}) - - def get_by_query(self, table, query): - """Get a specific record from a given table.""" - logger.debug("Querying table %s with query %s", table, query) - try: - result = self.resource(api_path=f"/table/{table}").get(query=query).one_or_none() - except requests.exceptions.HTTPError as exc: - # Raised if for example we get a 400 response because we're querying a nonexistent table - logger.error("HTTP error encountered: %s", exc) - result = None - - if not result: - logger.warning("Query %s did not match an object in table %s", query, table) - return result - - def ensure_choice(self, table, element, label, value=None): - """Ensure that the given choice value exists for the given element of the given table. - - Args: - table (str): Name of the table this choice belongs to - element (str): Name of the table field this choice belongs to - label (str): Human-readable label for this choice value. - value (str): Backend value of this choice. - """ - if not value: - value = label - - sys_choice = self.resource(api_path="/table/sys_choice") - query = {"name": table, "element": element, "label": label, "value": value} - record = sys_choice.get(query=query).one_or_none() - if record: - self.worker.unchanged("No changes to choice %r for field %r in table %r", label, element, table) - else: - if not self.worker.dry_run: - sys_choice.create(payload=query) - self.worker.created("Created choice %r for field %r in table %r", label, element, table) - - def ensure_field(self, table, field, datatype, **kwargs): - """Ensure that the given custom field exists in the given table. - - Args: - table (str): Name of the table to inspect/modify - field (str): Slug of the field to ensure - datatype (str): Datatype human-readable name ("string", "longint", etc.) as defined by ServiceNow. - **kwargs: Additional parameters to set on the field (``column_label``, ``max_length``, etc.) - """ - if "column_label" not in kwargs: - kwargs["column_label"] = field.replace("_", " ").title() - - # In all the examples I've found online, people are setting `internal_type` to a sys_id value. - # The below is how to find the sys_id for a given type such as string, longint, reference, etc. - # However, in my experimentation, just using the type name string seems to work just as well - # and is less error-prone. - # - # sys_glide = self.resource(api_path="/table/sys_glide_object") - # datatype_sys_id_record = sys_glide.get(query={"label": datatype.title()}).one_or_none() - # if not datatype_sys_id_record: - # logger.error("No datatype %r found", datatype) - # return - # datatype_sys_id = datatype_sys_id_record["sys_id"] - - sys_dict = self.resource(api_path="/table/sys_dictionary") - query = {"name": table, "element": field} - updates = {"internal_type": datatype, **kwargs} - record = sys_dict.get(query=query).one_or_none() - if record: - changed = update(record, updates) - if changed: - if not self.worker.dry_run: - record = sys_dict.update(query=query, payload=record) - self.worker.updated("Updated existing field %r in table %r", field, table) - else: - self.worker.unchanged("No changes to field %r in table %r", field, table) - else: - if not self.worker.dry_run: - record = sys_dict.create(payload={**query, **updates}) - self.worker.created("Added field %r to table %r", field, table) - - # TODO: can we also automatically add this field to the form layout for this table? - - def ensure_table(self, table_name, label=None, fields=()): - """Ensure that the given custom table exists and is correctly defined. - - Args: - table_name (str): Table slug - label (str): Human-readable label for this table - fields (list): List of (name, type, kwargs, choices) to pass through to :meth:`ensure_field` - """ - if not label: - label = table_name.upper() - - sys_db = self.resource(api_path="/table/sys_db_object") - query = {"name": table_name} - updates = {"label": label} - record = sys_db.get(query=query).one_or_none() - if record: - changed = update(record, updates) - if changed: - if not self.worker.dry_run: - record = sys_db.update(query=query, payload=record) - self.worker.updated("Updated existing table %r", table_name) - else: - self.worker.unchanged("No changes to definition of table %r", table_name) - else: - if not self.worker.dry_run: - record = sys_db.create(payload={**query, **updates}) - self.worker.created("Created table %s", table_name) - - for slug, datatype, kwargs, choices in fields: - # Create fields in this table. - self.ensure_field(table_name, slug, datatype, **kwargs) - if datatype == "choice": - for choice in choices: - self.ensure_choice(table_name, slug, choice) - elif choices: - logger.error("Choices are specified for non-choice field %r of type %s", slug, datatype) - - # TODO: can we also automatically add a link to this table view? diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/__init__.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/__init__.py deleted file mode 100644 index a359aeca9..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit tests for nautobot_ssot_servicenow plugin.""" diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_api.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_api.py deleted file mode 100644 index da743a47d..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_api.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Unit tests for nautobot_ssot_servicenow.""" -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient - -from nautobot.users.models import Token - -User = get_user_model() - - -class PlaceholderAPITest(TestCase): - """Test the nautobot_ssot_servicenow API.""" - - def setUp(self): - """Create a superuser and token for API calls.""" - self.user = User.objects.create(username="testuser", is_superuser=True) - self.token = Token.objects.create(user=self.user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") - - def test_placeholder(self): - """Verify that devices can be listed.""" - url = reverse("dcim-api:device-list") - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 0) diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_basic.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_basic.py deleted file mode 100644 index 8d4f9ef35..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/tests/test_basic.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Basic tests that do not require Django.""" -import unittest -import os -import toml - -from nautobot_ssot_servicenow import __version__ as project_version - - -class TestVersion(unittest.TestCase): - """Test Version is the same.""" - - def test_version(self): - """Verify that pyproject.toml version is same as version specified in the package.""" - parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) - poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"] - self.assertEqual(project_version, poetry_version) diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/urls.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/urls.py deleted file mode 100644 index 34cc866f7..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/urls.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Django urlpatterns declaration for nautobot_ssot_servicenow plugin.""" -# from django.urls import path - -urlpatterns = [] diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/utilities.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/utilities.py deleted file mode 100644 index c182e26e0..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/utilities.py +++ /dev/null @@ -1,93 +0,0 @@ -import os - -import usaddress -import yaml - -from jinja2 import Environment, FileSystemLoader - - -DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - - -def get_nested_attr(obj, attr): - """Like getattr, but handles nested attributes like "device_type.manufacturer.name". - - Additionally, returns None if no such attribute exists rather than throwing an exception. - """ - value = obj - for subattr in attr.split("."): - if hasattr(value, subattr): - value = getattr(value, subattr) - else: - return None - return value - - -def update(target, source): - """Update the target dictionary with data from source. - - Equivalent to calling target.update(source), but reports whether any changes occurred in target. - - Args: - target (dict): Dictionary to update - source (dict): Dictionary whose contents will be set into ``target``. - - Returns: - bool: True if ``target`` was modified at all. - """ - changed = False - for key, value in source.items(): - if key in target: - if target[key] == value: - continue # No change to this key - target[key] = value - changed = True - - return changed - - -def load_yaml_datafile(filename, config): - """Get the contents of the given YAML datafile. - - Args: - filename (str): Filename within the ``nautobot_ssot_servicenow/data/`` directory. - config (dict): Data for Jinja2 templating. - - Returns: - object: Parsed and populated data. - """ - file_path = os.path.join(DATA_DIR, filename) - if not os.path.isfile(file_path): - raise RuntimeError(f"No data file found at {file_path}") - env = Environment(loader=FileSystemLoader(DATA_DIR), autoescape=True) - template = env.get_template(filename) - populated = template.render(config) - return yaml.safe_load(populated) - - -def parse_physical_address(nb_site, field): - """Attempt to parse the free-text Nautobot "site.physical_address" into tokens and construct the requested field. - - Args: - nb_site (Site): Nautobot record that has a "physical_address". - field (str): Address field to retrieve. - """ - # usaddress.tag() returns a tuple (data, address_type) - data, _ = usaddress.tag(nb_site.physical_address) - if field == "street": - text = "" - for key in ("AddressNumber", "StreetName", "StreetNamePostType", "OccupancyType", "OccupancyIdentifier"): - if key in data: - if text: - text += " " - text += data[key] - return text - if field == "city": - return data.get("PlaceName", "") - if field == "state": - return data.get("StateName", "") - if field == "zip": - return data.get("ZipCode", "") - if field == "country": - return data.get("CountryName", "") - raise NotImplementedError diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/worker.py b/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/worker.py deleted file mode 100644 index b0c45c42d..000000000 --- a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/worker.py +++ /dev/null @@ -1,66 +0,0 @@ -import logging - -from diffsync.enum import DiffSyncFlags - -from nautobot.extras.jobs import StringVar - -from nautobot_ssot.sync.base import DataSyncWorker - -from .diffsync.adapter_nautobot import NautobotDiffSync -from .diffsync.adapter_servicenow import ServiceNowDiffSync -from .servicenow import ServiceNowClient - - -class ServiceNowExportDataSyncWorker(DataSyncWorker): - """Worker class to handle data sync to ServiceNow.""" - - snow_instance = StringVar( - label="ServiceNow instance", - description='<instance>.servicenow.com, such as "dev12345"' - ) - snow_username = StringVar( - label="ServiceNow username", - ) - snow_password = StringVar( - label="ServiceNow password", - # TODO widget=... - ) - snow_app_prefix = StringVar( - label="ServiceNow app prefix", - description="(if any)", - default="", - required=False, - ) - - class Meta: - name = "ServiceNow" - slug = "service-now" - description = "Synchronize data from Nautobot into ServiceNow." - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.snc = ServiceNowClient( - instance=self.data["snow_instance"], - username=self.data["snow_username"], - password=self.data["snow_password"], - app_prefix=self.data["snow_app_prefix"], - worker=self, - ) - self.servicenow_diffsync = ServiceNowDiffSync(client=self.snc, worker=self) - self.nautobot_diffsync = NautobotDiffSync() - - def execute(self): - """Sync a slew of Nautobot data into ServiceNow.""" - self.servicenow_diffsync.load() - self.nautobot_diffsync.load() - - diff = self.servicenow_diffsync.diff_from(self.nautobot_diffsync) - self.sync.diff = diff.dict() - self.sync.save() - if not self.dry_run: - self.servicenow_diffsync.sync_from( - self.nautobot_diffsync, - flags=DiffSyncFlags.CONTINUE_ON_FAILURE | - DiffSyncFlags.LOG_UNCHANGED_RECORDS | - DiffSyncFlags.SKIP_UNMATCHED_DST, - ) diff --git a/nautobot-plugin-ssot-servicenow/poetry.lock b/nautobot-plugin-ssot-servicenow/poetry.lock deleted file mode 100644 index d2e6ea395..000000000 --- a/nautobot-plugin-ssot-servicenow/poetry.lock +++ /dev/null @@ -1,1362 +0,0 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "asgiref" -version = "3.3.4" -description = "ASGI specs, helper code, and adapters" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] - -[[package]] -name = "astroid" -version = "2.5.6" -description = "An abstract syntax tree for Python with inference support." -category = "dev" -optional = false -python-versions = "~=3.6" - -[package.dependencies] -lazy-object-proxy = ">=1.4.0" -typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -wrapt = ">=1.11,<1.13" - -[[package]] -name = "bandit" -version = "1.7.0" -description = "Security oriented static analyser for python code." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -GitPython = ">=1.0.1" -PyYAML = ">=5.3.1" -six = ">=1.10.0" -stevedore = ">=1.20.0" - -[[package]] -name = "black" -version = "20.8b1" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -appdirs = "*" -click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} -mypy-extensions = ">=0.4.3" -pathspec = ">=0.6,<1" -regex = ">=2020.1.8" -toml = ">=0.10.1" -typed-ast = ">=1.4.0" -typing-extensions = ">=3.7.4" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -name = "certifi" -version = "2020.12.5" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "click" -version = "8.0.1" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "coverage" -version = "5.5" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -toml = ["toml"] - -[[package]] -name = "dataclasses" -version = "0.7" -description = "A backport of the dataclasses module for Python 3.6" -category = "main" -optional = false -python-versions = ">=3.6, <3.7" - -[[package]] -name = "diffsync" -version = "1.3.0" -description = "Library to easily sync/diff/update 2 different data sources" -category = "main" -optional = false -python-versions = ">=3.6,<4.0" - -[package.dependencies] -colorama = ">=0.4.3,<0.5.0" -dataclasses = {version = ">=0.7,<0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} -pydantic = ">=1.7.2,<2.0.0" -structlog = ">=20.1.0,<21.0.0" - -[[package]] -name = "django" -version = "3.1.11" -description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -asgiref = ">=3.2.10,<4" -pytz = "*" -sqlparse = ">=0.2.2" - -[package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "django-debug-toolbar" -version = "3.2.1" -description = "A configurable set of panels that display various debug information about the current request/response." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -Django = ">=2.2" -sqlparse = ">=0.2.0" - -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "gitdb" -version = "4.0.7" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.4" - -[package.dependencies] -smmap = ">=3.0.1,<5" - -[[package]] -name = "gitpython" -version = "3.1.17" -description = "Python Git Library" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} - -[[package]] -name = "idna" -version = "2.10" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "ijson" -version = "2.6.1" -description = "Iterative JSON parser with a standard Python iterator interface" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "importlib-metadata" -version = "3.4.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "invoke" -version = "1.5.0" -description = "Pythonic task execution" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "isort" -version = "5.8.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" - -[package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] - -[[package]] -name = "jinja2" -version = "2.11.3" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -MarkupSafe = ">=0.23" - -[package.extras] -i18n = ["Babel (>=0.8)"] - -[[package]] -name = "joblib" -version = "1.0.1" -description = "Lightweight pipelining with Python functions" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "lazy-object-proxy" -version = "1.6.0" -description = "A fast and thorough lazy object proxy." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[[package]] -name = "livereload" -version = "2.6.3" -description = "Python LiveReload is an awesome tool for web developers" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} - -[[package]] -name = "lunr" -version = "0.5.8" -description = "A Python implementation of Lunr.js" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -future = ">=0.16.0" -nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} -six = ">=1.11.0" - -[package.extras] -languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] - -[[package]] -name = "markdown" -version = "3.3.4" -description = "Python implementation of Markdown." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-include" -version = "0.6.0" -description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -markdown = "*" - -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mkdocs" -version = "1.1.2" -description = "Project documentation with Markdown." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -click = ">=3.3" -Jinja2 = ">=2.10.1" -livereload = ">=2.5.1" -lunr = {version = "0.5.8", extras = ["languages"]} -Markdown = ">=3.2.1" -PyYAML = ">=3.10" -tornado = ">=5.0" - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "nltk" -version = "3.6.2" -description = "Natural Language Toolkit" -category = "dev" -optional = false -python-versions = ">=3.5.*" - -[package.dependencies] -click = "*" -joblib = "*" -regex = "*" -tqdm = "*" - -[package.extras] -all = ["matplotlib", "twython", "scipy", "numpy", "gensim (<4.0.0)", "python-crfsuite", "pyparsing", "scikit-learn", "requests"] -corenlp = ["requests"] -machine_learning = ["gensim (<4.0.0)", "numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - -[[package]] -name = "oauthlib" -version = "3.1.0" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -rsa = ["cryptography"] -signals = ["blinker"] -signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] - -[[package]] -name = "pathspec" -version = "0.8.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pbr" -version = "5.6.0" -description = "Python Build Reasonableness" -category = "dev" -optional = false -python-versions = ">=2.6" - -[[package]] -name = "probableparsing" -version = "0.0.1" -description = "Common methods for propbable parsers" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pydantic" -version = "1.7.4" -description = "Data validation and settings management using python 3.6 type hinting" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] -typing_extensions = ["typing-extensions (>=3.7.2)"] - -[[package]] -name = "pydocstyle" -version = "6.1.1" -description = "Python docstring style checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -snowballstemmer = "*" - -[package.extras] -toml = ["toml"] - -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pylint" -version = "2.8.2" -description = "python code static checker" -category = "dev" -optional = false -python-versions = "~=3.6" - -[package.dependencies] -astroid = ">=2.5.6,<2.7" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.7" -toml = ">=0.7.1" - -[[package]] -name = "pylint-django" -version = "2.4.4" -description = "A Pylint plugin to help Pylint understand the Django web framework" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pylint = ">=2.0" -pylint-plugin-utils = ">=0.5" - -[package.extras] -for_tests = ["django-tables2", "factory-boy", "coverage", "pytest"] -with_django = ["django"] - -[[package]] -name = "pylint-plugin-utils" -version = "0.6" -description = "Utilities and helpers for writing Pylint plugins" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pylint = ">=1.7" - -[[package]] -name = "pysnow" -version = "0.7.17" -description = "ServiceNow HTTP client library" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -ijson = ">=2.5.1,<3.0.0" -oauthlib = ">=3.1.0,<4.0.0" -python-magic = ">=0.4.15,<0.5.0" -pytz = ">=2019.3,<2020.0" -requests = ">=2.21.0,<3.0.0" -requests-oauthlib = ">=1.3.0,<2.0.0" -six = ">=1.13.0,<2.0.0" - -[[package]] -name = "python-crfsuite" -version = "0.9.7" -description = "Python binding for CRFsuite" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "python-magic" -version = "0.4.22" -description = "File type identification using libmagic" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pytz" -version = "2019.3" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyyaml" -version = "5.4.1" -description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[[package]] -name = "regex" -version = "2021.4.4" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "requests" -version = "2.25.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] - -[[package]] -name = "requests-oauthlib" -version = "1.3.0" -description = "OAuthlib authentication support for Requests." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "smmap" -version = "4.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "snowballstemmer" -version = "2.1.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "sqlparse" -version = "0.4.1" -description = "A non-validating SQL parser." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "stevedore" -version = "3.3.0" -description = "Manage dynamic plugins for Python applications" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - -[[package]] -name = "structlog" -version = "20.2.0" -description = "Structured Logging for Python" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] -docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] -tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "tornado" -version = "6.1" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -category = "dev" -optional = false -python-versions = ">= 3.5" - -[[package]] -name = "tqdm" -version = "4.61.0" -description = "Fast, Extensible Progress Meter" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] -telegram = ["requests"] - -[[package]] -name = "typed-ast" -version = "1.4.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "urllib3" -version = "1.26.4" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] - -[[package]] -name = "usaddress" -version = "0.5.10" -description = "Parse US addresses using conditional random fields" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -future = ">=0.14" -probableparsing = "*" -python-crfsuite = ">=0.7" - -[[package]] -name = "wrapt" -version = "1.12.1" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "yamllint" -version = "1.26.1" -description = "A linter for YAML files." -category = "dev" -optional = false -python-versions = ">=3.5.*" - -[package.dependencies] -pathspec = ">=0.5.3" -pyyaml = "*" - -[[package]] -name = "zipp" -version = "3.4.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.6" -content-hash = "9e48593c0d66bc2466a225e78aa4399b66cb2b0bbf2b502f77c74e6c41ecc50f" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -asgiref = [ - {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, - {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, -] -astroid = [ - {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, - {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, -] -bandit = [ - {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, - {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, -] -black = [ - {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, -] -certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, -] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] -click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, -] -dataclasses = [ - {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, - {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, -] -diffsync = [ - {file = "diffsync-1.3.0-py3-none-any.whl", hash = "sha256:c23bac1080ea7205272bacb4ae4659a23a96172a28024febe162a5559e178d0b"}, - {file = "diffsync-1.3.0.tar.gz", hash = "sha256:ab5499293e307f872056a757bea22e0c88ec91e88dfdba4d6bdc59832835b2af"}, -] -django = [ - {file = "Django-3.1.11-py3-none-any.whl", hash = "sha256:c79245c488411d1ae300b8f7a08ac18a496380204cf3035aff97ad917a8de999"}, - {file = "Django-3.1.11.tar.gz", hash = "sha256:9a0a2f3d34c53032578b54db7ec55929b87dda6fec27a06cc2587afbea1965e5"}, -] -django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, - {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] -gitdb = [ - {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, - {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, -] -gitpython = [ - {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, - {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -ijson = [ - {file = "ijson-2.6.1-cp27-cp27m-macosx_10_6_x86_64.whl", hash = "sha256:60393946d73792d5adeeaa25e82ff2f5bf19b17f6617a468743a4db4a07298a0"}, - {file = "ijson-2.6.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d320dc1c1c9adbe404668b0fed6bfa0ac8693911159564f4655a5f2059746993"}, - {file = "ijson-2.6.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a4cd7f8ecf035d0e23db1cc6d6036e6c563f31abacbceae88904bb8b7f88b1f6"}, - {file = "ijson-2.6.1-cp34-cp34m-macosx_10_6_x86_64.whl", hash = "sha256:9904bf55bc1f170353c32144861d8295f0bdc41034e5e6ae58cbf30610023ca6"}, - {file = "ijson-2.6.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7bac04b23691e6ab122d8f9ff06b26dbbb6df01babbf6bf8856ccad1c505278b"}, - {file = "ijson-2.6.1-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:ae9cc3ebbe8fa030b923b5dff912a61980edd03dc00b92f5c0223e44cbc51d9f"}, - {file = "ijson-2.6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8ce67b7d3435c3fc831d5c06f60b2d20a853b599cdf885478e575a3416fbf655"}, - {file = "ijson-2.6.1-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:a8b486bdf24e389947e588f4021498f6cc56cafdfaec1c78e9952e0f338aef23"}, - {file = "ijson-2.6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0042bb768fb890c177af923c0ead157cdc70c6dfa64827765c1a3676a879190"}, - {file = "ijson-2.6.1-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:26978c02314233c87bddad8800b7b9a56a052334f495e2bce93b282397c6931d"}, - {file = "ijson-2.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:25d4d159405f75a7443c1fe83b6d7be5a7da017b4aa9cc1bb5cda3feb74aaf32"}, - {file = "ijson-2.6.1.tar.gz", hash = "sha256:75ebc60b23abfb1c97f475ab5d07a5ed725ad4bd1f58513d8b258c21f02703d0"}, -] -importlib-metadata = [ - {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, - {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, -] -invoke = [ - {file = "invoke-1.5.0-py2-none-any.whl", hash = "sha256:da7c2d0be71be83ffd6337e078ef9643f41240024d6b2659e7b46e0b251e339f"}, - {file = "invoke-1.5.0-py3-none-any.whl", hash = "sha256:7e44d98a7dc00c91c79bac9e3007276965d2c96884b3c22077a9f04042bd6d90"}, - {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"}, -] -isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, -] -jinja2 = [ - {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, - {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, -] -joblib = [ - {file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"}, - {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, - {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, - {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, - {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, - {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, - {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, -] -livereload = [ - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] -lunr = [ - {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, - {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, -] -markdown = [ - {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, - {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, -] -markdown-include = [ - {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"}, -] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mkdocs = [ - {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, - {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -nltk = [ - {file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"}, - {file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"}, -] -oauthlib = [ - {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, - {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, -] -pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, -] -pbr = [ - {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, - {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, -] -probableparsing = [ - {file = "probableparsing-0.0.1-py2.py3-none-any.whl", hash = "sha256:509df25fdda4fd7c0b2a100f58cc971bd23daf26f3b3320aebf2616d2e10c69e"}, - {file = "probableparsing-0.0.1.tar.gz", hash = "sha256:8114bbf889e1f9456fe35946454c96e42a6ee2673a90d4f1f9c46a406f543767"}, -] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] -pydantic = [ - {file = "pydantic-1.7.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3c60039e84552442defbcb5d56711ef0e057028ca7bfc559374917408a88d84e"}, - {file = "pydantic-1.7.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6e7e314acb170e143c6f3912f93f2ec80a96aa2009ee681356b7ce20d57e5c62"}, - {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:8ef77cd17b73b5ba46788d040c0e820e49a2d80cfcd66fda3ba8be31094fd146"}, - {file = "pydantic-1.7.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:115d8aa6f257a1d469c66b6bfc7aaf04cd87c25095f24542065c68ebcb42fe63"}, - {file = "pydantic-1.7.4-cp36-cp36m-win_amd64.whl", hash = "sha256:66757d4e1eab69a3cfd3114480cc1d72b6dd847c4d30e676ae838c6740fdd146"}, - {file = "pydantic-1.7.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c92863263e4bd89e4f9cf1ab70d918170c51bd96305fe7b00853d80660acb26"}, - {file = "pydantic-1.7.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3b8154babf30a5e0fa3aa91f188356763749d9b30f7f211fafb247d4256d7877"}, - {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:80cc46378505f7ff202879dcffe4bfbf776c15675028f6e08d1d10bdfbb168ac"}, - {file = "pydantic-1.7.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:dda60d7878a5af2d8560c55c7c47a8908344aa78d32ec1c02d742ede09c534df"}, - {file = "pydantic-1.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4c1979d5cc3e14b35f0825caddea5a243dd6085e2a7539c006bc46997ef7a61a"}, - {file = "pydantic-1.7.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8857576600c32aa488f18d30833aa833b54a48e3bab3adb6de97e463af71f8f8"}, - {file = "pydantic-1.7.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f86d4da363badb39426a0ff494bf1d8510cd2f7274f460eee37bdbf2fd495ec"}, - {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3ea1256a9e782149381e8200119f3e2edea7cd6b123f1c79ab4bbefe4d9ba2c9"}, - {file = "pydantic-1.7.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e28455b42a0465a7bf2cde5eab530389226ce7dc779de28d17b8377245982b1e"}, - {file = "pydantic-1.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:47c5b1d44934375a3311891cabd450c150a31cf5c22e84aa172967bf186718be"}, - {file = "pydantic-1.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00250e5123dd0b123ff72be0e1b69140e0b0b9e404d15be3846b77c6f1b1e387"}, - {file = "pydantic-1.7.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d24aa3f7f791a023888976b600f2f389d3713e4f23b7a4c88217d3fce61cdffc"}, - {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2c44a9afd4c4c850885436a4209376857989aaf0853c7b118bb2e628d4b78c4e"}, - {file = "pydantic-1.7.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:e87edd753da0ca1d44e308a1b1034859ffeab1f4a4492276bff9e1c3230db4fe"}, - {file = "pydantic-1.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:a3026ee105b5360855e500b4abf1a1d0b034d88e75a2d0d66a4c35e60858e15b"}, - {file = "pydantic-1.7.4-py3-none-any.whl", hash = "sha256:a82385c6d5a77e3387e94612e3e34b77e13c39ff1295c26e3ba664e7b98073e2"}, - {file = "pydantic-1.7.4.tar.gz", hash = "sha256:0a1abcbd525fbb52da58c813d54c2ec706c31a91afdb75411a73dd1dec036595"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] -pylint = [ - {file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"}, - {file = "pylint-2.8.2.tar.gz", hash = "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217"}, -] -pylint-django = [ - {file = "pylint-django-2.4.4.tar.gz", hash = "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"}, - {file = "pylint_django-2.4.4-py3-none-any.whl", hash = "sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b"}, -] -pylint-plugin-utils = [ - {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, - {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, -] -pysnow = [ - {file = "pysnow-0.7.17-py2.py3-none-any.whl", hash = "sha256:c73b28a0d6b7a28518e7271b4af6cc94052985cec19e71bc7715e55c4d765862"}, - {file = "pysnow-0.7.17.tar.gz", hash = "sha256:9ca04d53b897999426854ab03577a9fdbefe925526373a2fd647ee551fe520ca"}, -] -python-crfsuite = [ - {file = "python-crfsuite-0.9.7.tar.gz", hash = "sha256:3b4538d2ce5007e4e42005818247bf43ade89ef08a66d158462e2f7c5d63cee7"}, - {file = "python_crfsuite-0.9.7-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:cd18b340c5a45ec200e8ce1167318dfc5d915ca9aad459dfa8675d014fd30650"}, - {file = "python_crfsuite-0.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d386d3a1e8d2065b4770c0dd06877ac28d7a94f61cd8447af3fa7a49551e98f9"}, - {file = "python_crfsuite-0.9.7-cp27-cp27m-win32.whl", hash = "sha256:2390c7cf62c72179b96c130048cec981173d3873ded532f739ba5ff770ed2d39"}, - {file = "python_crfsuite-0.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:bb57e551d86c83ec6a719c9884c571cb9a9b013a78fe0c317b0677c3c9542965"}, - {file = "python_crfsuite-0.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b46811138745d9d62ff7674bc7a14a9cc974c065dadfc6f78e0dc19832066ec2"}, - {file = "python_crfsuite-0.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4c9effa3cf7087cfecaa91ccada1ff9998b276bbde285700ef405345890253b1"}, - {file = "python_crfsuite-0.9.7-cp35-cp35m-win32.whl", hash = "sha256:b3da774cedf542202533b014347b86fbc25191356f0d5568f9784f8eb77e7ef6"}, - {file = "python_crfsuite-0.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:5ebb57783a0723d46d82d462fbfd6111e62e48533bfe1fbcd5ffb8dc1ba7a573"}, - {file = "python_crfsuite-0.9.7-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9934e684ff89ae97be52971c4c127329b1e1604ada9f903c7427a7062f256fc6"}, - {file = "python_crfsuite-0.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c56f34b50049de3127353214af45bc9b437fd6c23202b83abb0b8052d86a248b"}, - {file = "python_crfsuite-0.9.7-cp36-cp36m-win32.whl", hash = "sha256:8704a6b7c7c64c4aa158125c89e9e08377a0169e83c75094aa65833b771d3078"}, - {file = "python_crfsuite-0.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:1d2faa31771df2370bcf15855aa403416d14f088d3e81b19de857ea013a697b0"}, - {file = "python_crfsuite-0.9.7-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:d03ca82d34b45c6efa8f086eb05c7217e4a7fed34640e714775deaa08b61e6d2"}, - {file = "python_crfsuite-0.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0dc0149a62764e7d24d4f1a362f51b02e0283ac2b2469ce7f36666ece0b55855"}, - {file = "python_crfsuite-0.9.7-cp37-cp37m-win32.whl", hash = "sha256:b1568ab4c7a97f54b4d57f5b9795a4d6d841f7dc7923dd40414e34a93500cc42"}, - {file = "python_crfsuite-0.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:e905914a688138c29205a6752e768965ef3b0bfc46102b4a94316fd00dac7bc2"}, - {file = "python_crfsuite-0.9.7-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:397bac9cd4bae7a1b27d215c0119d33ff51c4ec5343d1f474867fd1a04c18a1d"}, - {file = "python_crfsuite-0.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:946ef3481c8dcd7c331123dd39b227cc52a386322967db78db650c58a6c972df"}, - {file = "python_crfsuite-0.9.7-cp38-cp38-win32.whl", hash = "sha256:df9edb37c90744c3aafd5d7dbf7c50fc486fe189e0e85a1deaf7af995ecac7b5"}, - {file = "python_crfsuite-0.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:caa980287a90fd8c659c6d936f5f4a5b28d0157ce530ad90a6430faed1cf147f"}, - {file = "python_crfsuite-0.9.7-py2.7-win-amd64.egg", hash = "sha256:a14959d27475f379711798e1cbdad79ebcab07976ea52d5b4862c36132ae16f5"}, - {file = "python_crfsuite-0.9.7-py2.7-win32.egg", hash = "sha256:9e8b03b02866c23e9618245757cf70cbdef18b9ce0893121c23ccd114fb78508"}, - {file = "python_crfsuite-0.9.7-py3.5-win-amd64.egg", hash = "sha256:09faa4425b9d8c128946c68c58c8efd5f28908ddf6b941af97475e2072f61495"}, - {file = "python_crfsuite-0.9.7-py3.5-win32.egg", hash = "sha256:4753c42cdd6c7f48ea745943f641c23d87a9547d22a07ea45903702cea1c7be2"}, - {file = "python_crfsuite-0.9.7-py3.6-win-amd64.egg", hash = "sha256:9aede38a4c93c90b9fa1b291c2e12521bcf718d6900beae0f933667f184c68ba"}, - {file = "python_crfsuite-0.9.7-py3.6-win32.egg", hash = "sha256:dfbfbfc298057e56532151910f042bb4b579502037d9403627a72cc51d572961"}, - {file = "python_crfsuite-0.9.7-py3.7-win-amd64.egg", hash = "sha256:ac25832a8ab55f3a0a91c863e7f4f270ccac9d34b2bf1e2ac457fc8e97c81ba2"}, - {file = "python_crfsuite-0.9.7-py3.7-win32.egg", hash = "sha256:468bcb736a98627df89708f631cfd0e0c5c7825b545ea1a1e91d7db2bbad88a6"}, - {file = "python_crfsuite-0.9.7-py3.8-win-amd64.egg", hash = "sha256:5cff06b51c16594ab4132d72a8b4b381ff4351a1825e388e120739c223ca849e"}, - {file = "python_crfsuite-0.9.7-py3.8-win32.egg", hash = "sha256:263f29c656fbb63d8d198d30ec9bca5b6fc7fab61fd20dd2f7cab795a613a85a"}, -] -python-magic = [ - {file = "python-magic-0.4.22.tar.gz", hash = "sha256:ca884349f2c92ce830e3f498c5b7c7051fe2942c3ee4332f65213b8ebff15a62"}, - {file = "python_magic-0.4.22-py2.py3-none-any.whl", hash = "sha256:8551e804c09a3398790bd9e392acb26554ae2609f29c72abb0b9dee9a5571eae"}, -] -pytz = [ - {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, - {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, -] -pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, -] -regex = [ - {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, - {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, - {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, - {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, - {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, - {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, - {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, - {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, - {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, - {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, - {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, - {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, - {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, -] -requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, -] -requests-oauthlib = [ - {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, - {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, - {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -smmap = [ - {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, - {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, -] -sqlparse = [ - {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, - {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, -] -stevedore = [ - {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, - {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, -] -structlog = [ - {file = "structlog-20.2.0-py2.py3-none-any.whl", hash = "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd"}, - {file = "structlog-20.2.0.tar.gz", hash = "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tornado = [ - {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, - {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, - {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, - {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, - {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, - {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, - {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, - {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, - {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, - {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, - {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, - {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, - {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, - {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, - {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, - {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, - {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, - {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, -] -tqdm = [ - {file = "tqdm-4.61.0-py2.py3-none-any.whl", hash = "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493"}, - {file = "tqdm-4.61.0.tar.gz", hash = "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042"}, -] -typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, -] -typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, -] -urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, -] -usaddress = [ - {file = "usaddress-0.5.10-py2.py3-none-any.whl", hash = "sha256:4d66b11fdbec84f18d1bac8614fea2d1be1bb027e55849b5e2c1057bbca17437"}, - {file = "usaddress-0.5.10.tar.gz", hash = "sha256:1a8ebf62d0cce58d7d8286dde70373c530a9317b6fe1752a4197b75b7d0870e3"}, -] -wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, -] -yamllint = [ - {file = "yamllint-1.26.1.tar.gz", hash = "sha256:87d9462b3ed7e9dfa19caa177f7a77cd9888b3dc4044447d6ae0ab233bcd1324"}, -] -zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, -] diff --git a/nautobot-plugin-ssot-servicenow/pyproject.toml b/nautobot-plugin-ssot-servicenow/pyproject.toml deleted file mode 100644 index e37121b0a..000000000 --- a/nautobot-plugin-ssot-servicenow/pyproject.toml +++ /dev/null @@ -1,100 +0,0 @@ -[tool.poetry] -name = "nautobot-ssot-servicenow" -version = "0.1.0" -description = "Nautobot SSoT ServiceNow" -authors = ["Network to Code, LLC "] - -license = "Apache-2.0" - -readme = "README.md" -homepage = "https://github.com/nautobot/nautobot-plugin-ssot-servicenow" -repository = "https://github.com/nautobot/nautobot-plugin-ssot-servicenow" -keywords = ["nautobot", "nautobot-plugin"] -include = [ - "LICENSE", - "README.md", -] -packages = [ - { include = "nautobot_ssot_servicenow" }, -] - -[tool.poetry.dependencies] -python = "^3.6" -Jinja2 = "<3" -pysnow = "^0.7.17" -usaddress = "^0.5.10" -PyYAML = "^5.4.1" -diffsync = "^1.3.0" - -[tool.poetry.dev-dependencies] -invoke = "*" -black = "*" -django-debug-toolbar = "*" -yamllint = "*" -bandit = "*" -pylint = "*" -pylint-django = "*" -pydocstyle = "*" -flake8 = "*" -coverage = "*" -mkdocs = "*" -markdown-include = "*" - -[tool.poetry.plugins."nautobot_ssot.data_targets"] -"ServiceNow" = "nautobot_ssot_servicenow.worker:ServiceNowExportDataSyncWorker" - -[tool.black] -line-length = 120 -target-version = ['py37'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | settings.py # This is where you define files that should not be stylized by black - # the root of the project -) -''' - -[tool.pylint.master] -# Include the pylint_django plugin to avoid spurious warnings about Django patterns -load-plugins="pylint_django" - -[tool.pylint.basic] -# No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. -no-docstring-rgx="^(_|test_|Meta$)" - -[tool.pylint.messages_control] -# Line length is enforced by Black, so pylint doesn't need to check it. -# Pylint and Black disagree about how to format multi-line arrays; Black wins. -disable = """, - line-too-long, - bad-continuation, - """ - -[tool.pylint.miscellaneous] -# Don't flag TODO as a failure, let us commit with things that still need to be done in the code -notes = """, - FIXME, - XXX, - """ - -[build-system] -requires = ["poetry_core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - -[tool.pytest.ini_options] -testpaths = [ - "tests" -] -addopts = "-vv --doctest-modules" diff --git a/nautobot-plugin-ssot-servicenow/tasks.py b/nautobot-plugin-ssot-servicenow/tasks.py deleted file mode 100644 index 7a1fdd73c..000000000 --- a/nautobot-plugin-ssot-servicenow/tasks.py +++ /dev/null @@ -1,373 +0,0 @@ -"""Tasks for use with Invoke. - -(c) 2020-2021 Network To Code -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -from distutils.util import strtobool -from invoke import Collection, task as invoke_task -import os - - -def is_truthy(arg): - """Convert "truthy" strings into Booleans. - - Examples: - >>> is_truthy('yes') - True - Args: - arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, - f, false, off and 0. Raises ValueError if val is anything else. - """ - if isinstance(arg, bool): - return arg - return bool(strtobool(arg)) - - -# Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html -# Variables may be overwritten in invoke.yml or by the environment variables INVOKE_NAUTOBOT-DATA-SYNC-SERVICENOW_xxx -namespace = Collection("nautobot_ssot_servicenow") -namespace.configure( - { - "nautobot_ssot_servicenow": { - "nautobot_ver": "develop-latest", - "project_name": "nautobot-ssot-servicenow", - "python_ver": "3.6", - "local": False, - "compose_dir": os.path.join(os.path.dirname(__file__), "development"), - "compose_files": [ - "docker-compose.requirements.yml", - "docker-compose.base.yml", - "docker-compose.dev.yml", - "docker-compose.docs.yml", - ], - } - } -) - - -def task(function=None, *args, **kwargs): - """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" - - def task_wrapper(function=None): - """Wrapper around invoke.task to add the task to the namespace as well.""" - if args or kwargs: - task_func = invoke_task(*args, **kwargs)(function) - else: - task_func = invoke_task(function) - namespace.add_task(task_func) - return task_func - - if function: - # The decorator was called with no arguments - return task_wrapper(function) - # The decorator was called with arguments - return task_wrapper - - -def docker_compose(context, command, **kwargs): - """Helper function for running a specific docker-compose command with all appropriate parameters and environment. - - Args: - context (obj): Used to run specific commands - command (str): Command string to append to the "docker-compose ..." command, such as "build", "up", etc. - **kwargs: Passed through to the context.run() call. - """ - build_env = {"NAUTOBOT_VER": context.nautobot_ssot_servicenow.nautobot_ver, "PYTHON_VER": context.nautobot_ssot_servicenow.python_ver} - compose_command = f'docker-compose --project-name {context.nautobot_ssot_servicenow.project_name} --project-directory "{context.nautobot_ssot_servicenow.compose_dir}"' - for compose_file in context.nautobot_ssot_servicenow.compose_files: - compose_file_path = os.path.join(context.nautobot_ssot_servicenow.compose_dir, compose_file) - compose_command += f' -f "{compose_file_path}"' - compose_command += f" {command}" - print(f'Running docker-compose command "{command}"') - return context.run(compose_command, env=build_env, **kwargs) - - -def run_command(context, command, **kwargs): - """Wrapper to run a command locally or inside the nautobot container.""" - if is_truthy(context.nautobot_ssot_servicenow.local): - context.run(command, **kwargs) - else: - # Check if netbox is running, no need to start another netbox container to run a command - docker_compose_status = "ps --services --filter status=running" - results = docker_compose(context, docker_compose_status, hide="out") - if "nautobot" in results.stdout: - compose_command = f"exec nautobot {command}" - else: - compose_command = f"run --entrypoint '{command}' nautobot" - - docker_compose(context, compose_command, pty=True) - - -# ------------------------------------------------------------------------------ -# BUILD -# ------------------------------------------------------------------------------ -@task( - help={ - "force_rm": "Always remove intermediate containers", - "cache": "Whether to use Docker's cache when building the image (defaults to enabled)", - } -) -def build(context, force_rm=False, cache=True): - """Build Nautobot docker image.""" - command = "build" - - if not cache: - command += " --no-cache" - if force_rm: - command += " --force-rm" - - print(f"Building Nautobot with Python {context.nautobot_ssot_servicenow.python_ver}...") - docker_compose(context, command) - - -@task -def generate_packages(context): - """Generate all Python packages inside docker and copy the file locally under dist/.""" - command = "poetry build" - run_command(context, command) - - -# ------------------------------------------------------------------------------ -# START / STOP / DEBUG -# ------------------------------------------------------------------------------ -@task -def debug(context): - """Start Nautobot and its dependencies in debug mode.""" - print("Starting Nautobot in debug mode...") - docker_compose(context, "up") - - -@task -def start(context): - """Start Nautobot and its dependencies in detached mode.""" - print("Starting Nautobot in detached mode...") - docker_compose(context, "up --detach") - - -@task -def restart(context): - """Gracefully restart all containers.""" - print("Restarting Nautobot...") - docker_compose(context, "restart") - - -@task -def stop(context): - """Stop Nautobot and its dependencies.""" - print("Stopping Nautobot...") - docker_compose(context, "down") - - -@task -def destroy(context): - """Destroy all containers and volumes.""" - print("Destroying Nautobot...") - docker_compose(context, "down --volumes") - - -@task -def vscode(context): - """Launch Visual Studio Code with the appropriate Environment variables to run in a container.""" - command = "code nautobot.code-workspace" - - context.run(command) - - -# ------------------------------------------------------------------------------ -# ACTIONS -# ------------------------------------------------------------------------------ -@task -def nbshell(context): - """Launch an interactive nbshell session.""" - command = "nautobot-server nbshell" - run_command(context, command) - - -@task -def cli(context): - """Launch a bash shell inside the running Nautobot container.""" - run_command(context, "bash") - - -@task( - help={ - "user": "name of the superuser to create (default: admin)", - } -) -def createsuperuser(context, user="admin"): - """Create a new Nautobot superuser account (default: "admin"), will prompt for password.""" - command = f"nautobot-server createsuperuser --username {user}" - - run_command(context, command) - - -@task( - help={ - "name": "name of the migration to be created; if unspecified, will autogenerate a name", - } -) -def makemigrations(context, name=""): - """Perform makemigrations operation in Django.""" - command = "nautobot-server makemigrations nautobot_ssot_servicenow" - - if name: - command += f" --name {name}" - - run_command(context, command) - - -@task -def migrate(context): - """Perform migrate operation in Django.""" - command = "nautobot-server migrate" - - run_command(context, command) - - -@task(help={}) -def post_upgrade(context): - """ - Performs Nautobot common post-upgrade operations using a single entrypoint. - - This will run the following management commands with default settings, in order: - - - migrate - - trace_paths - - collectstatic - - remove_stale_contenttypes - - clearsessions - - invalidate all - """ - command = "nautobot-server post_upgrade" - - run_command(context, command) - - -# ------------------------------------------------------------------------------ -# TESTS -# ------------------------------------------------------------------------------ -@task( - help={ - "autoformat": "Apply formatting recommendations automatically, rather than failing if formatting is incorrect.", - } -) -def black(context, autoformat=False): - """Check Python code style with Black.""" - if autoformat: - black_command = "black" - else: - black_command = "black --check --diff" - - command = f"{black_command} ." - - run_command(context, command) - - -@task -def flake8(context): - """Check for PEP8 compliance and other style issues.""" - command = "flake8 ." - run_command(context, command) - - -@task -def hadolint(context): - """Check Dockerfile for hadolint compliance and other style issues.""" - command = "hadolint development/Dockerfile" - run_command(context, command) - - -@task -def pylint(context): - """Run pylint code analysis.""" - command = 'pylint --init-hook "import nautobot; nautobot.setup()" --rcfile pyproject.toml nautobot_ssot_servicenow' - run_command(context, command) - - -@task -def pydocstyle(context): - """Run pydocstyle to validate docstring formatting adheres to NTC defined standards.""" - # We exclude the /migrations/ directory since it is autogenerated code - command = "pydocstyle --config=.pydocstyle.ini ." - run_command(context, command) - - -@task -def bandit(context): - """Run bandit to validate basic static code security analysis.""" - command = "bandit --recursive . --configfile .bandit.yml" - run_command(context, command) - - -@task -def check_migrations(context): - """Check for missing migrations.""" - command = "nautobot-server --config=nautobot/core/tests/nautobot_config.py makemigrations --dry-run --check" - - run_command(context, command) - - -@task( - help={ - "keepdb": "save and re-use test database between test runs for faster re-testing.", - "label": "specify a directory or module to test instead of running all Nautobot tests", - "failfast": "fail as soon as a single test fails don't run the entire test suite", - "buffer": "Discard output from passing tests", - } -) -def unittest(context, keepdb=False, label="nautobot_ssot_servicenow", failfast=False, buffer=True): - """Run Nautobot unit tests.""" - command = f"coverage run --module nautobot.core.cli test {label}" - - if keepdb: - command += " --keepdb" - if failfast: - command += " --failfast" - if buffer: - command += " --buffer" - run_command(context, command) - - -@task -def unittest_coverage(context): - """Report on code test coverage as measured by 'invoke unittest'.""" - command = "coverage report --skip-covered --include 'nautobot_ssot_servicenow/*' --omit *migrations*" - - run_command(context, command) - - -@task( - help={ - "failfast": "fail as soon as a single test fails don't run the entire test suite", - } -) -def tests(context, failfast=False): - """Run all tests for this plugin.""" - # If we are not running locally, start the docker containers so we don't have to for each test - if not is_truthy(context.nautobot_ssot_servicenow.local): - print("Starting Docker Containers...") - start(context) - # Sorted loosely from fastest to slowest - print("Running black...") - black(context) - print("Running flake8...") - flake8(context) - print("Running bandit...") - bandit(context) - print("Running pydocstyle...") - pydocstyle(context) - print("Running pylint...") - pylint(context) - print("Running unit tests...") - unittest(context, failfast=failfast) - print("All tests have passed!") - unittest_coverage(context) diff --git a/nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/__init__.py b/packages/placeholder similarity index 100% rename from nautobot-plugin-ssot-servicenow/nautobot_ssot_servicenow/diffsync/__init__.py rename to packages/placeholder From c304c6a7abfcc6f406dacb5f84fbe76933d372eb Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Jun 2021 17:36:49 -0400 Subject: [PATCH 07/42] Some refactoring and point fixes --- nautobot_ssot/__init__.py | 2 +- nautobot_ssot/models.py | 6 +- nautobot_ssot/sync/__init__.py | 105 ++---------------- nautobot_ssot/sync/example.py | 25 +++-- nautobot_ssot/sync/job.py | 94 ++++++++++++++++ nautobot_ssot/sync/{base.py => worker.py} | 10 +- .../templates/nautobot_ssot/sync_detail.html | 2 +- nautobot_ssot/views.py | 4 +- 8 files changed, 129 insertions(+), 119 deletions(-) create mode 100644 nautobot_ssot/sync/job.py rename nautobot_ssot/sync/{base.py => worker.py} (92%) diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index a58961e4c..657086d1c 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -9,7 +9,7 @@ class NautobotSSOTPluginConfig(PluginConfig): """Plugin configuration for the nautobot_ssot plugin.""" name = "nautobot_ssot" - verbose_name = "SSoT" + verbose_name = "Single Source of Truth" version = __version__ author = "Network to Code, LLC" description = "Nautobot Single Source of Truth" diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index 42e7b7ac3..23ed29160 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -101,7 +101,7 @@ def get_source_url(self): def get_target_url(self): """Get the absolute url of the target worker associated with this instance.""" - if self.source == "Nautobot": + if self.target == "Nautobot": return None return reverse( "plugins:nautobot_ssot:sync_add_target", @@ -142,10 +142,6 @@ class SyncLogEntry(BaseModel): message = models.CharField(max_length=511, blank=True) - @property - def dry_run(self): - return self.overview.dry_run - class Meta: verbose_name_plural = "sync log entries" ordering = ["sync", "timestamp"] diff --git a/nautobot_ssot/sync/__init__.py b/nautobot_ssot/sync/__init__.py index 25d95243c..14aef5a08 100644 --- a/nautobot_ssot/sync/__init__.py +++ b/nautobot_ssot/sync/__init__.py @@ -1,49 +1,10 @@ -"""Worker code for executing DataSyncWorkers.""" - -import logging - +"""Basic API for data sync sources/targets (workers).""" import pkg_resources -from django.utils import timezone - -from django_rq import job -import structlog - -from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices - -from nautobot_ssot.choices import SyncLogEntryActionChoices - - -logger = logging.getLogger("rq.worker") - - -def log_to_log_entry(_logger, _log_method, event_dict): - """Capture certain structlog messages from DiffSync into the Nautobot database.""" - # TODO rework this - from nautobot_ssot.models import SyncLogEntry - if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): - sync = event_dict["src"].sync - sync_worker = event_dict["src"].sync_worker - object_repr = event_dict["unique_id"] - # The DiffSync log gives us a model name (string) and unique_id (string). - # Try to look up the actual Nautobot object that this describes. - changed_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) - if changed_object: - object_repr = repr(changed_object) - SyncLogEntry.objects.create( - sync=sync, - action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, - diff=event_dict["diffs"], - status=event_dict["status"], - message=event_dict["event"], - changed_object=changed_object, - object_change=object_change, - object_repr=object_repr, - ) - return event_dict def get_data_sources(): """Get a list of registered sync worker data sources.""" + # TODO: add option for caching to avoid unnecessary re-loading return sorted( [ entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_sources") @@ -51,8 +12,10 @@ def get_data_sources(): key=lambda worker: worker.name, ) + def get_data_targets(): """Get a list of registered sync worker data targets.""" + # TODO: add option for caching to avoid unnecessary re-loading return sorted( [ entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_targets") @@ -60,8 +23,10 @@ def get_data_targets(): key=lambda worker: worker.name, ) + def get_data_source(name=None, slug=None): """Look up the specified data source class.""" + # TODO: add option for caching to avoid unnecessary re-loading for data_source in get_data_sources(): if name and data_source.name != name: continue @@ -70,8 +35,10 @@ def get_data_source(name=None, slug=None): return data_source raise KeyError(f'No data source "{name or slug}" found!') + def get_data_target(name=None, slug=None): """Look up the specified data target class.""" + # TODO: add option for caching to avoid unnecessary re-loading for data_target in get_data_targets(): if name and data_target.name != name: continue @@ -79,59 +46,3 @@ def get_data_target(name=None, slug=None): continue return data_target raise KeyError(f'No data target "{name or slug}" found!') - -@job("default") -def sync(sync_id, data): - """Perform a requested sync.""" - # TODO rework this - from nautobot_ssot.models import Sync - sync = Sync.objects.get(id=sync_id) - - sync.job_result.log( - f"START: data synchronization {sync}", grouping="sync", level_choice=LogLevelChoices.LOG_INFO, logger=logger - ) - sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) - sync.job_result.save() - sync.start_time = timezone.now() - sync.save() - - try: - structlog.configure( - processors=[ - log_to_log_entry, - structlog.stdlib.render_to_log_kwargs, - ], - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - if sync.source != "Nautobot": - sync_worker = get_data_source(name=sync.source)(sync=sync, data=data) - else: - sync_worker = get_data_target(name=sync.target)(sync=sync, data=data) - - sync_worker.execute() - - except Exception as exc: - sync.job_result.log( - f"Exception occurred during {sync}: {exc}", - grouping="sync", - level_choice=LogLevelChoices.LOG_FAILURE, - logger=logger, - ) - sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) - else: - sync.job_result.log( - f"FINISH: data synchronization {sync}", - grouping="sync", - level_choice=LogLevelChoices.LOG_INFO, - logger=logger, - ) - sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) - finally: - sync.job_result.completed = timezone.now() - sync.job_result.save() - - return {"ok": sync.job_result.status == JobResultStatusChoices.STATUS_COMPLETED} diff --git a/nautobot_ssot/sync/example.py b/nautobot_ssot/sync/example.py index 9cd04879b..4eb6d599f 100644 --- a/nautobot_ssot/sync/example.py +++ b/nautobot_ssot/sync/example.py @@ -2,12 +2,12 @@ from django.db import transaction -from nautobot.dcim.models import Site, RackGroup, Rack +from nautobot.dcim.models import Site from nautobot.extras.jobs import StringVar from nautobot.utilities.exceptions import AbortTransaction from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices -from nautobot_ssot.sync.base import DataSyncWorker +from nautobot_ssot.sync.worker import DataSyncWorker class ExampleSyncWorker(DataSyncWorker): @@ -17,25 +17,28 @@ class ExampleSyncWorker(DataSyncWorker): class Meta: name = "Example Sync Worker" slug = "example-sync-worker" - description = "An example of how a sync worker might be implemented" + description = "An example of how a sync worker might be implemented." - def execute(self, dry_run=True): + def execute(self): """Perform a mock data synchronization.""" # For sake of a simple example, we don't actually use DiffSync here + self.job_log(f"Beginning execution, dry_run = {self.dry_run}") try: with transaction.atomic(): site, created = Site.objects.get_or_create( slug=self.data["site_slug"], defaults={"name": self.data["site_slug"]}, ) - if dry_run: + action = SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE + self.sync_log( + action=action, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + changed_object=site, + ) + if self.dry_run: + # Note that this is not an ideal way to implement dry-run behavior; + # most notably it will also revert any JobResult changes or SyncLogEntry records created above! raise AbortTransaction() except AbortTransaction: self.job_log("Database changes have been reverted automatically.") - - self.sync_log( - action=SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE, - status=SyncLogEntryStatusChoices.STATUS_SUCCESS, - changed_object=site, - ) diff --git a/nautobot_ssot/sync/job.py b/nautobot_ssot/sync/job.py new file mode 100644 index 000000000..ca90ed4fb --- /dev/null +++ b/nautobot_ssot/sync/job.py @@ -0,0 +1,94 @@ +"""Job API for invoking a sync worker.""" +import logging + +from django.utils import timezone + +from django_rq import job +import structlog + +from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices + +from nautobot_ssot.choices import SyncLogEntryActionChoices +from nautobot_ssot.models import Sync, SyncLogEntry +from nautobot_ssot.sync import get_data_source, get_data_target + + +logger = logging.getLogger("rq.worker") + + +@job("default") +def sync(sync_id, data): + """Perform a requested sync.""" + sync = Sync.objects.get(id=sync_id) + + sync.job_result.log( + f"START: data synchronization {sync}", grouping="sync", level_choice=LogLevelChoices.LOG_INFO, logger=logger + ) + sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) + sync.job_result.save() + sync.start_time = timezone.now() + sync.save() + + def structlog_to_log_entry(_logger, _log_method, event_dict): + """Capture certain structlog messages from DiffSync into the Nautobot database.""" + if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): + sync = event_dict["src"].sync + sync_worker = event_dict["src"].sync_worker + object_repr = event_dict["unique_id"] + # The DiffSync log gives us a model name (string) and unique_id (string). + # Try to look up the actual Nautobot object that this describes. + changed_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) + if changed_object: + object_repr = repr(changed_object) + SyncLogEntry.objects.create( + sync=sync, + action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, + diff=event_dict["diffs"], + status=event_dict["status"], + message=event_dict["event"], + changed_object=changed_object, + object_change=object_change, + object_repr=object_repr, + ) + return event_dict + + try: + structlog.configure( + processors=[ + structlog_to_log_entry, + structlog.stdlib.render_to_log_kwargs, + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + if sync.source != "Nautobot": + sync_worker = get_data_source(name=sync.source)(sync=sync, data=data) + else: + sync_worker = get_data_target(name=sync.target)(sync=sync, data=data) + + sync_worker.execute() + + except Exception as exc: + sync.job_result.log( + f"Exception occurred during {sync}: {exc}", + grouping="sync", + level_choice=LogLevelChoices.LOG_FAILURE, + logger=logger, + ) + sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) + else: + sync.job_result.log( + f"FINISH: data synchronization {sync}", + grouping="sync", + level_choice=LogLevelChoices.LOG_INFO, + logger=logger, + ) + sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) + finally: + sync.job_result.completed = timezone.now() + sync.job_result.save() + + return {"ok": sync.job_result.status == JobResultStatusChoices.STATUS_COMPLETED} diff --git a/nautobot_ssot/sync/base.py b/nautobot_ssot/sync/worker.py similarity index 92% rename from nautobot_ssot/sync/base.py rename to nautobot_ssot/sync/worker.py index 4f59c37de..20fb342e8 100644 --- a/nautobot_ssot/sync/base.py +++ b/nautobot_ssot/sync/worker.py @@ -1,5 +1,4 @@ """Base/generic functionality for implementation of a data synchronization worker class.""" - from collections import OrderedDict from django.utils.functional import classproperty @@ -131,11 +130,18 @@ def sync_log( # def execute(self): - """Perform a dry run or actual data synchronization, depending on self.dry_run.""" + """Perform a dry run or actual data synchronization, depending on self.dry_run. + + Note that unlike a Nautobot Job, it is up to the implementation to provide dry-run behavior; + since execution may involve manipulation of data in other systems, Nautobot cannot itself + guarantee proper rollback or other dry-run behavior on its own. + """ def lookup_object(self, model_name, unique_id): """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. + Optional helper method used to build more detailed/accurate SyncLogEntry records from DiffSync logs. + Args: model_name (str): DiffSyncModel class name or similar class/model label. unique_id (str): DiffSyncModel unique_id or similar unique identifier. diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index f3fcfbb99..0ebfb4577 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -7,7 +7,7 @@
diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index fb0484f8c..08dc0b1cf 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -116,7 +116,7 @@ def post(self, request, slug, kind="source"): form = sync_worker_class.as_form(request.POST, request.FILES) if form.is_valid(): - dry_run = form.cleaned_data.pop("dry_run") + dry_run = form.cleaned_data.get("dry_run", True) sync = Sync.objects.create(source=source, target=target, dry_run=dry_run, diff={}) job_result = JobResult.objects.create( @@ -130,7 +130,7 @@ def post(self, request, slug, kind="source"): transaction.on_commit( lambda: get_queue("default").enqueue( - "nautobot_ssot.sync.sync", sync_id=sync.pk, data=form.cleaned_data, job_timeout=3600 + "nautobot_ssot.sync.job.sync", sync_id=sync.pk, data=form.cleaned_data, job_timeout=3600 ) ) From 198a7fe79d016a5c26f0014d192a6b47a2a1d0bd Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 8 Jun 2021 11:42:17 -0400 Subject: [PATCH 08/42] Log traceback if exception occurs during sync --- nautobot_ssot/sync/job.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nautobot_ssot/sync/job.py b/nautobot_ssot/sync/job.py index ca90ed4fb..33240d4bb 100644 --- a/nautobot_ssot/sync/job.py +++ b/nautobot_ssot/sync/job.py @@ -1,5 +1,6 @@ """Job API for invoking a sync worker.""" import logging +import traceback from django.utils import timezone @@ -78,6 +79,7 @@ def structlog_to_log_entry(_logger, _log_method, event_dict): level_choice=LogLevelChoices.LOG_FAILURE, logger=logger, ) + logger.error(traceback.format_exc()) sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) else: sync.job_result.log( From 6c50e2f6f64913d078b0d0b122c7380088b702bc Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 10 Jun 2021 16:01:38 -0400 Subject: [PATCH 09/42] Rename changed_object field to synced_object to be more accurate --- nautobot_ssot/filters.py | 2 +- nautobot_ssot/migrations/0002_refinements.py | 28 ++++++++++++++++++++ nautobot_ssot/models.py | 13 +++++---- nautobot_ssot/sync/example.py | 2 +- nautobot_ssot/sync/job.py | 10 +++---- nautobot_ssot/sync/worker.py | 8 +++--- nautobot_ssot/tables.py | 10 ++++--- 7 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 nautobot_ssot/migrations/0002_refinements.py diff --git a/nautobot_ssot/filters.py b/nautobot_ssot/filters.py index 76f87dfb2..eff2937a0 100644 --- a/nautobot_ssot/filters.py +++ b/nautobot_ssot/filters.py @@ -23,7 +23,7 @@ class SyncLogEntryFilter(BaseFilterSet): class Meta: model = SyncLogEntry - fields = ["sync", "action", "status", "changed_object_type"] + fields = ["sync", "action", "status", "synced_object_type"] def search(self, queryset, name, value): if not value.strip(): diff --git a/nautobot_ssot/migrations/0002_refinements.py b/nautobot_ssot/migrations/0002_refinements.py new file mode 100644 index 000000000..f6f61c9cc --- /dev/null +++ b/nautobot_ssot/migrations/0002_refinements.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.11 on 2021-06-09 14:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nautobot_ssot', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='synclogentry', + old_name='changed_object_id', + new_name='synced_object_id', + ), + migrations.RenameField( + model_name='synclogentry', + old_name='changed_object_type', + new_name='synced_object_type', + ), + migrations.AlterField( + model_name='synclogentry', + name='diff', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index 23ed29160..a9020875c 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -114,11 +114,14 @@ class SyncLogEntry(BaseModel): Detailed sync logs are recorded in this model, rather than in JobResult.data, because JobResult.data imposes fairly strict expectations about the structure of its contents - that do not align well with the requirements of this plugin. + that do not align well with the requirements of this plugin. Also, storing log entries as individual + database records rather than a single JSON blob allows us to filter, query, sort, etc. as desired. This model somewhat "shadows" Nautobot's built-in ObjectChange model; the key distinction to bear in mind is that an ObjectChange reflects a change that *did happen*, while a SyncLogEntry may reflect this or may reflect a change that *could not happen* or *failed*. + Additionally, if we're syncing data from Nautobot to a different system as data target, + the data isn't changing in Nautobot, so there will be no ObjectChange record. """ sync = models.ForeignKey(to=Sync, on_delete=models.CASCADE, related_name="logs", related_query_name="log") @@ -126,16 +129,16 @@ class SyncLogEntry(BaseModel): action = models.CharField(max_length=32, choices=SyncLogEntryActionChoices) status = models.CharField(max_length=32, choices=SyncLogEntryStatusChoices) - diff = models.JSONField() + diff = models.JSONField(blank=True, null=True) - changed_object_type = models.ForeignKey( + synced_object_type = models.ForeignKey( to=ContentType, blank=True, null=True, on_delete=models.PROTECT, ) - changed_object_id = models.UUIDField(blank=True, null=True) - changed_object = GenericForeignKey(ct_field="changed_object_type", fk_field="changed_object_id") + synced_object_id = models.UUIDField(blank=True, null=True) + synced_object = GenericForeignKey(ct_field="synced_object_type", fk_field="synced_object_id") object_repr = models.CharField(max_length=200, editable=False) object_change = models.ForeignKey(to=ObjectChange, on_delete=models.SET_NULL, blank=True, null=True) diff --git a/nautobot_ssot/sync/example.py b/nautobot_ssot/sync/example.py index 4eb6d599f..37360dad0 100644 --- a/nautobot_ssot/sync/example.py +++ b/nautobot_ssot/sync/example.py @@ -34,7 +34,7 @@ def execute(self): self.sync_log( action=action, status=SyncLogEntryStatusChoices.STATUS_SUCCESS, - changed_object=site, + synced_object=site, ) if self.dry_run: # Note that this is not an ideal way to implement dry-run behavior; diff --git a/nautobot_ssot/sync/job.py b/nautobot_ssot/sync/job.py index 33240d4bb..542856bd1 100644 --- a/nautobot_ssot/sync/job.py +++ b/nautobot_ssot/sync/job.py @@ -38,16 +38,16 @@ def structlog_to_log_entry(_logger, _log_method, event_dict): object_repr = event_dict["unique_id"] # The DiffSync log gives us a model name (string) and unique_id (string). # Try to look up the actual Nautobot object that this describes. - changed_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) - if changed_object: - object_repr = repr(changed_object) + synced_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) + if synced_object: + object_repr = repr(synced_object) SyncLogEntry.objects.create( sync=sync, action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, - diff=event_dict["diffs"], + diff=event_dict["diffs"] if event_dict["action"] else None, status=event_dict["status"], message=event_dict["event"], - changed_object=changed_object, + synced_object=synced_object, object_change=object_change, object_repr=object_repr, ) diff --git a/nautobot_ssot/sync/worker.py b/nautobot_ssot/sync/worker.py index 20fb342e8..32b233c4d 100644 --- a/nautobot_ssot/sync/worker.py +++ b/nautobot_ssot/sync/worker.py @@ -104,15 +104,15 @@ def sync_log( status, message="", diff=None, - changed_object=None, + synced_object=None, object_repr=None, object_change=None, ): """Log a action message as a SyncLogEntry.""" if not diff: diff = {} - if changed_object and not object_repr: - object_repr = repr(changed_object) + if synced_object and not object_repr: + object_repr = repr(synced_object) SyncLogEntry.objects.create( sync=self.sync, @@ -120,7 +120,7 @@ def sync_log( status=status, message=message, diff=diff, - changed_object=changed_object, + synced_object=synced_object, object_repr=object_repr, object_change=object_change, ) diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index bf91df98e..7b1682928 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -142,14 +142,16 @@ class SyncLogEntryTable(BaseTable): """Table for displaying SyncLogEntry records.""" pk = ToggleColumn() - sync = LinkColumn(accessor="sync__id", verbose_name="Sync") + sync = LinkColumn(verbose_name="Sync") action = TemplateColumn(template_code=ACTION_LABEL) - diff = JSONColumn(orderable=False) status = TemplateColumn(template_code=LOG_STATUS_LABEL) + diff = JSONColumn(orderable=False) message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) - changed_object = LinkColumn(verbose_name="Changed object") + synced_object = LinkColumn(verbose_name="Synced Object") + object_change = LinkColumn() class Meta(BaseTable.Meta): model = SyncLogEntry - fields = ("pk", "timestamp", "sync", "action", "changed_object", "diff", "status", "message") + fields = ("pk", "timestamp", "sync", "action", "synced_object_type", "synced_object", "object_repr", "object_change", "status", "diff", "message") + default_columns = ("pk", "timestamp", "sync", "action", "synced_object", "status", "diff", "message") order_by = ("-timestamp",) From 697717e65bb85685e906b1e761401fed1b47f959 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 10 Jun 2021 16:02:09 -0400 Subject: [PATCH 10/42] Breadcrumb fixup --- nautobot_ssot/templates/nautobot_ssot/sync_run.html | 2 +- .../templates/nautobot_ssot/synclogentry_list.html | 13 +++++++++++++ nautobot_ssot/views.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 nautobot_ssot/templates/nautobot_ssot/synclogentry_list.html diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_run.html b/nautobot_ssot/templates/nautobot_ssot/sync_run.html index 96aef7fa1..91e9d98e2 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_run.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_run.html @@ -8,7 +8,7 @@
diff --git a/nautobot_ssot/templates/nautobot_ssot/synclogentry_list.html b/nautobot_ssot/templates/nautobot_ssot/synclogentry_list.html new file mode 100644 index 000000000..c173a7035 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/synclogentry_list.html @@ -0,0 +1,13 @@ +{% extends 'generic/object_list.html' %} + +{% block header %} +
+
+ +
+
+{% endblock %} +{% block title %}SSoT Sync Logs{% endblock %} diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 08dc0b1cf..ed96a07dd 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -180,3 +180,4 @@ class SyncLogEntryListView(ObjectListView): filterset_form = SyncLogEntryFilterForm table = SyncLogEntryTable action_buttons = [] + template_name = "nautobot_ssot/synclogentry_list.html" From a39a034f0b57005597fdf97cbc7f872128f2556b Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 10 Jun 2021 17:02:51 -0400 Subject: [PATCH 11/42] Improve datetime rendering in str(Sync) --- nautobot_ssot/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index a9020875c..cc22f583d 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -18,10 +18,12 @@ """ from datetime import timedelta +from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse +from django.utils.formats import date_format from django.utils.timezone import now from nautobot.core.models import BaseModel @@ -54,7 +56,7 @@ class Meta: ordering = ["start_time"] def __str__(self): - return f"{self.source} -> {self.target}, {self.start_time}" + return f"{self.source} -> {self.target}, {date_format(self.start_time, format=settings.SHORT_DATETIME_FORMAT)}" def get_absolute_url(self): return reverse("plugins:nautobot_ssot:sync", kwargs={"pk": self.pk}) From 9e6f50b73b330236d33b25a7e348768edc591bad Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 10 Jun 2021 17:38:46 -0400 Subject: [PATCH 12/42] Improve sync detail view --- nautobot_ssot/models.py | 2 +- .../templates/nautobot_ssot/sync_detail.html | 91 ++++++++++++++++++- nautobot_ssot/views.py | 2 +- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index cc22f583d..355ec52bd 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -56,7 +56,7 @@ class Meta: ordering = ["start_time"] def __str__(self): - return f"{self.source} -> {self.target}, {date_format(self.start_time, format=settings.SHORT_DATETIME_FORMAT)}" + return f"{self.source} → {self.target}, {date_format(self.start_time, format=settings.SHORT_DATETIME_FORMAT)}" def get_absolute_url(self): return reverse("plugins:nautobot_ssot:sync", kwargs={"pk": self.pk}) diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index 0ebfb4577..82aa828b6 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load plugins %} +{% load shorter_timedelta %} {% block header %}
@@ -42,16 +43,96 @@

{% block title %}{{ object }}{% endblock %}

- Summary + Sync
- - + + + + + + + + + + + + + + + + + + + + + + + + + + - + + +
Dry run?{{ object.dry_run }}Data Source + {% if object.get_source_url %} + {{ object.source }} + {% else %} + {{ object.source }} + {% endif %} +
Data Target + {% if object.get_target_url %} + {{ object.target }} + {% else %} + {{ object.target }} + {% endif %} +
Sync or Dry run? + {% if object.dry_run %} + Dry Run + {% else %} + Sync + {% endif %} +
Start Time{{ object.start_time }} ({{ object.start_time | timesince }} ago)
End Time{{ object.job_result.completed }} ({{ object.job_result.completed | timesince }} ago)
Duration{{ object.duration | shorter_timedelta }}
Status{{ object.job_result.get_status_display }}
Job result{{ object.job_result }}{{ object.job_result.pk }}
+
+
+
+ Statistics +
+ + + + + + + +
Actions Taken + + + + +
Outcomes + + + +
@@ -64,7 +145,7 @@

{% block title %}{{ object }}{% endblock %}

Diff
-
{{ diff }}
+
{{ diff }}
{% plugin_right_page object %}
diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index ed96a07dd..573fd6d3f 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -162,7 +162,7 @@ class SyncBulkDeleteView(BulkDeleteView): class SyncView(ObjectView): """View for details of a single Sync record.""" - queryset = Sync.objects.all() + queryset = Sync.queryset() template_name = "nautobot_ssot/sync_detail.html" def get_extra_context(self, request, instance): From 64f073f490b063647e533306a538f8ced9458504 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 14 Jun 2021 14:41:24 -0400 Subject: [PATCH 13/42] Various content --- development/Dockerfile | 3 +- .../templates/nautobot_ssot/sync_detail.html | 79 ++++--------------- .../templates/nautobot_ssot/sync_header.html | 44 +++++++++++ .../nautobot_ssot/sync_jobresult.html | 13 +++ .../nautobot_ssot/sync_logentries.html | 10 +++ nautobot_ssot/urls.py | 6 +- nautobot_ssot/views.py | 37 ++++++++- tasks.py | 2 +- 8 files changed, 125 insertions(+), 69 deletions(-) create mode 100644 nautobot_ssot/templates/nautobot_ssot/sync_header.html create mode 100644 nautobot_ssot/templates/nautobot_ssot/sync_jobresult.html create mode 100644 nautobot_ssot/templates/nautobot_ssot/sync_logentries.html diff --git a/development/Dockerfile b/development/Dockerfile index 710f73888..40bab004c 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -1,8 +1,7 @@ ARG PYTHON_VER ARG NAUTOBOT_VER -# FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} -FROM nniehoff/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} +FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} WORKDIR /source diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index 82aa828b6..beef934f3 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -1,43 +1,8 @@ -{% extends 'base.html' %} +{% extends 'nautobot_ssot/sync_header.html' %} {% load buttons %} -{% load custom_links %} {% load plugins %} {% load shorter_timedelta %} -{% block header %} -
-
- -
-
-
- {% plugin_buttons object %} - {% if perms.nautobot_ssot.delete_sync %} - {% delete_button object %} - {% endif %} -
-

{% block title %}{{ object }}{% endblock %}

- {% include 'inc/created_updated.html' %} -
- {% custom_links object %} -
- -{% endblock %} - {% block content %}
@@ -105,33 +70,33 @@

{% block title %}{{ object }}{% endblock %}

- -
Actions Taken - + {{ object.num_created }} - - + {{ object.num_updated }} - - + {{ object.num_deleted }} - - + {{ object.num_unchanged }} - +
Outcomes - + {{ object.num_succeeded }} - - + {{ object.num_failed }} - - + {{ object.num_errored }} - +
@@ -150,14 +115,4 @@

{% block title %}{{ object }}{% endblock %}

{% plugin_right_page object %}
-
-
- {% include "extras/inc/jobresult.html" with result=object.job_result %} - {% plugin_full_width_page object %} -
-
-{% endblock %} - -{% block javascript %} - {% include 'extras/inc/jobresult_js.html' with result=object.job_result %} {% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_header.html b/nautobot_ssot/templates/nautobot_ssot/sync_header.html new file mode 100644 index 000000000..461b0935f --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/sync_header.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% load buttons %} +{% load custom_links %} +{% load plugins %} + +{% block header %} +
+
+ +
+
+
+ {% plugin_buttons object %} + {% if perms.nautobot_ssot.delete_sync %} + {% delete_button object %} + {% endif %} +
+

{% block title %}{{ object }}{% endblock %}

+ {% include 'inc/created_updated.html' %} +
+ {% custom_links object %} +
+ +{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_jobresult.html b/nautobot_ssot/templates/nautobot_ssot/sync_jobresult.html new file mode 100644 index 000000000..077c70b09 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/sync_jobresult.html @@ -0,0 +1,13 @@ +{% extends 'nautobot_ssot/sync_header.html' %} + +{% block content %} +
+
+ {% include "extras/inc/jobresult.html" with result=object.job_result %} +
+
+{% endblock %} + +{% block javascript %} + {% include 'extras/inc/jobresult_js.html' with result=object.job_result %} +{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html b/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html new file mode 100644 index 000000000..7d97d6b19 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html @@ -0,0 +1,10 @@ +{% extends 'nautobot_ssot/sync_header.html' %} +{% load buttons %} + +{% block content %} +
+
+ {% include "responsive_table.html" %} +
+
+{% endblock %} diff --git a/nautobot_ssot/urls.py b/nautobot_ssot/urls.py index bba2286bc..11921e620 100644 --- a/nautobot_ssot/urls.py +++ b/nautobot_ssot/urls.py @@ -2,8 +2,6 @@ from django.urls import path -from nautobot.extras.views import ObjectChangeLogView - from . import models, views urlpatterns = [ @@ -25,10 +23,12 @@ path("history//", views.SyncView.as_view(), name="sync"), path( "history//changelog/", - ObjectChangeLogView.as_view(), + views.SyncChangeLogView.as_view(), name="sync_changelog", kwargs={"model": models.Sync}, ), path("history//delete/", views.SyncDeleteView.as_view(), name="sync_delete"), + path("history//jobresult/", views.SyncJobResultView.as_view(), name="sync_jobresult"), + path("history//logs/", views.SyncLogEntriesView.as_view(), name="sync_logentries"), path("logs/", views.SyncLogEntryListView.as_view(), name="synclogentry_list"), ] diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 573fd6d3f..c08585fe4 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -6,13 +6,14 @@ from django.contrib import messages from django.db import transaction from django.http import Http404 -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django_rq import get_queue from django_rq.queues import get_connection from rq import Worker from nautobot.extras.models import JobResult +from nautobot.extras.views import ObjectChangeLogView from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectView from .filters import SyncFilter, SyncLogEntryFilter @@ -172,6 +173,40 @@ def get_extra_context(self, request, instance): } +class SyncJobResultView(ObjectView): + """View for the JobResult associated with a single Sync record.""" + + queryset = Sync.objects.all() + template_name = "nautobot_ssot/sync_jobresult.html" + + def get_extra_context(self, request, instance): + """Add additional context to the view.""" + return { + "active_tab": "jobresult", + } + + +class SyncLogEntriesView(ObjectListView): + """View for SyncLogEntries associated with a given Sync.""" + + queryset = SyncLogEntry.objects.all() + filterset = SyncLogEntryFilter + filterset_form = SyncLogEntryFilterForm + table = SyncLogEntryTable + action_buttons = [] + template_name = "nautobot_ssot/sync_logentries.html" + + def get(self, request, pk): + instance = get_object_or_404(Sync.objects.all(), pk=pk) + self.queryset = SyncLogEntry.objects.filter(sync=instance) + + return super().get(request) + + +class SyncChangeLogView(ObjectChangeLogView): + base_template = "nautobot_ssot/sync_header.html" + + class SyncLogEntryListView(ObjectListView): """View for listing SyncLogEntry records.""" diff --git a/tasks.py b/tasks.py index 2622046b1..f41838cfb 100644 --- a/tasks.py +++ b/tasks.py @@ -39,7 +39,7 @@ def is_truthy(arg): namespace.configure( { "nautobot_ssot": { - "nautobot_ver": "develop-latest", + "nautobot_ver": "1.0.2", "project_name": "nautobot-ssot", "python_ver": "3.6", "local": False, From 8b108c30146ee3b18492ff93a819a621b27bbf39 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 15 Jun 2021 14:12:42 -0400 Subject: [PATCH 14/42] Refactor to more closely align with Jobs core functionality --- nautobot_ssot/jobs/__init__.py | 22 +++ nautobot_ssot/jobs/base.py | 167 ++++++++++++++++++ nautobot_ssot/jobs/examples.py | 52 ++++++ nautobot_ssot/models.py | 9 +- nautobot_ssot/sync/__init__.py | 48 ----- nautobot_ssot/sync/example.py | 44 ----- nautobot_ssot/sync/job.py | 96 ---------- nautobot_ssot/sync/worker.py | 152 ---------------- nautobot_ssot/tables.py | 3 +- nautobot_ssot/template_content.py | 26 +++ .../templates/nautobot_ssot/dashboard.html | 4 +- nautobot_ssot/urls.py | 12 -- nautobot_ssot/views.py | 95 ++-------- 13 files changed, 285 insertions(+), 445 deletions(-) create mode 100644 nautobot_ssot/jobs/__init__.py create mode 100644 nautobot_ssot/jobs/base.py create mode 100644 nautobot_ssot/jobs/examples.py delete mode 100644 nautobot_ssot/sync/__init__.py delete mode 100644 nautobot_ssot/sync/example.py delete mode 100644 nautobot_ssot/sync/job.py delete mode 100644 nautobot_ssot/sync/worker.py create mode 100644 nautobot_ssot/template_content.py diff --git a/nautobot_ssot/jobs/__init__.py b/nautobot_ssot/jobs/__init__.py new file mode 100644 index 000000000..e85afe412 --- /dev/null +++ b/nautobot_ssot/jobs/__init__.py @@ -0,0 +1,22 @@ +from nautobot.extras.jobs import get_jobs + +from .base import DataSource, DataTarget +from .examples import ExampleDataSource, ExampleDataTarget + +jobs = [ExampleDataSource, ExampleDataTarget] + + +def get_data_jobs(): + """Get all data-source and data-target jobs available.""" + jobs_dict = get_jobs() + data_sources = [] + data_targets = [] + for modules in jobs_dict.values(): + for module_data in modules.values(): + for job_class in module_data["jobs"].values(): + if issubclass(job_class, DataSource): + data_sources.append(job_class) + if issubclass(job_class, DataTarget): + data_targets.append(job_class) + + return (data_sources, data_targets) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py new file mode 100644 index 000000000..d314c6a77 --- /dev/null +++ b/nautobot_ssot/jobs/base.py @@ -0,0 +1,167 @@ +"""Base Job classes for sync workers.""" + +from django.forms import HiddenInput +from django.utils import timezone + +import structlog + +from nautobot.extras.jobs import BaseJob, BooleanVar + +from nautobot_ssot.choices import SyncLogEntryActionChoices +from nautobot_ssot.models import Sync, SyncLogEntry + + +class DataSyncBaseJob(BaseJob): + """Common base class for data synchronization jobs. + + Works mostly as per the BaseJob API, with the following changes: + + - Concrete subclasses are responsible for implementing `self.sync_data()`, **not** `self.run()`. + - Additional Meta field `dry_run_default` is available to be defined if desired. + - Meta fields `data_source` and `data_target` can be defined for lookup purposes + """ + + dry_run = BooleanVar() + + def sync_data(self): + """Method to be implemented by data sync concrete Job implementations. + + Available instance attributes include: + + - self.kwargs (corresponds to the Job's `data` input, including 'dry_run' option) + - self.commit (should generally be True) + - self.sync (Sync instance tracking this job execution) + - self.job_result (as per Job API) + """ + pass + + def lookup_object(self, model_name, unique_id): + """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. + + Optional helper method used to build more detailed/accurate SyncLogEntry records from DiffSync logs. + + Args: + model_name (str): DiffSyncModel class name or similar class/model label. + unique_id (str): DiffSyncModel unique_id or similar unique identifier. + + Returns: + tuple: (nautobot_record, nautobot_objectchange_record). Either or both may be None. + """ + return (None, None) + + def sync_log( + self, + action, + status, + message="", + diff=None, + synced_object=None, + object_repr=None, + object_change=None, + ): + """Log a action message as a SyncLogEntry.""" + if synced_object and not object_repr: + object_repr = repr(synced_object) + + SyncLogEntry.objects.create( + sync=self.sync, + action=action, + status=status, + message=message, + diff=diff, + synced_object=synced_object, + object_repr=object_repr, + object_change=object_change, + ) + + def _structlog_to_sync_log_entry(self, _logger, _log_method, event_dict): + """Capture certain structlog messages from DiffSync into the Nautobot database.""" + if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): + # The DiffSync log gives us a model name (string) and unique_id (string). + # Try to look up the actual Nautobot object that this describes. + synced_object, object_change = self.lookup_object(event_dict["model"], event_dict["unique_id"]) + self.sync_log( + action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, + diff=event_dict["diffs"] if event_dict["action"] else None, + status=event_dict["status"], + message=event_dict["event"], + synced_object=synced_object, + object_change=object_change, + ) + + return event_dict + + @classmethod + def _get_vars(cls): + """Extend Job._get_vars() to include `dry_run` variable. + + Workaround for https://github.com/netbox-community/netbox/issues/5529 + """ + vars = super()._get_vars() + if hasattr(cls, "dry_run"): + vars['dry_run'] = cls.dry_run + return vars + + def as_form(self, data=None, files=None, initial=None): + """Render this instance as a Django form for user inputs, including a "Dry run" field.""" + form = super().as_form(data=data, files=files, initial=initial) + # Set the "dry_run" widget's initial value based on our Meta attribute, if any + form.fields["dry_run"].initial = getattr(self.Meta, "dry_run_default", True) + # Hide the "commit" widget to reduce user confusion + form.fields["_commit"].widget = HiddenInput() + return form + + @property + def data_source(self): + """The system or data source providing input data for this sync.""" + return getattr(self.Meta, "data_source", self.name) + + @property + def data_target(self): + """The system or data source being modified by this sync.""" + return getattr(self.Meta, "data_target", self.name) + + def run(self, data, commit): + """Job entry point from Nautobot - do not override!""" + self.sync = Sync.objects.create( + source=self.data_source, + target=self.data_target, + dry_run=data["dry_run"], + job_result=self.job_result, + start_time=timezone.now(), + diff={}, + ) + + # Add _structlog_to_sync_log_entry as a processor for structlog calls from DiffSync + structlog.configure( + processors=[self._structlog_to_sync_log_entry, structlog.stdlib.render_to_log_kwargs], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + self.kwargs = data + self.commit = commit + + self.sync_data() + + +class DataSource(DataSyncBaseJob): + """Base class for Jobs that sync data **from** another data source **to** Nautobot.""" + + dry_run = BooleanVar(description="Perform a dry-run, making no actual changes to Nautobot data.") + + @property + def data_target(self): + return "Nautobot" + + +class DataTarget(DataSyncBaseJob): + """Base class for Jobs that sync data **to** another data target **from** Nautobot.""" + + dry_run = BooleanVar(description="Perform a dry-run, making no actual changes to the remote system.") + + @property + def data_source(self): + return "Nautobot" diff --git a/nautobot_ssot/jobs/examples.py b/nautobot_ssot/jobs/examples.py new file mode 100644 index 000000000..01205b096 --- /dev/null +++ b/nautobot_ssot/jobs/examples.py @@ -0,0 +1,52 @@ +from nautobot.dcim.models import Site +from nautobot.extras.jobs import StringVar, Job + +from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from nautobot_ssot.jobs.base import DataSource, DataTarget + + +class ExampleDataSource(DataSource, Job): + """An example data-source Job for loading data into Nautobot.""" + + site_slug = StringVar(description="Site to create or update", default="") + + class Meta: + name = "Example Data Source" + description = "An example of a 'data source' Job for loading data into Nautobot from elsewhere." + data_source = "Dummy Data" + + def sync_data(self): + """Perform data sync into Nautobot.""" + # For sake of a simple example, we don't actually use DiffSync here. + site, created = Site.objects.get_or_create( + slug=self.kwargs["site_slug"], + defaults={"name": self.kwargs["site_slug"]}, + ) + action = SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE + self.sync_log( + action=action, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + synced_object=site, + ) + + +class ExampleDataTarget(DataTarget, Job): + """An example data-target Job for loading data from Nautobot into another system.""" + + site_slug = StringVar(description="Site to sync to an imaginary data target", default="") + + class Meta: + name = "Example Data Target" + description = "An example of a 'data target' Job for loading data from Nautobot into elsewhere." + data_target = "Dummy Data" + + def sync_data(self): + """Perform data sync from Nautobot.""" + # For sake of a simple example, we don't actually use DiffSync here. + site = Site.objects.get(slug=self.kwargs["site_slug"]) + + self.sync_log( + action=SyncLogEntryActionChoices.ACTION_UPDATE, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + synced_object=site, + ) diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index 355ec52bd..f635a54b1 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -30,7 +30,6 @@ from nautobot.extras.models import ChangeLoggedModel, CustomFieldModel, JobResult, ObjectChange, RelationshipModel from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices -from .sync import get_data_source, get_data_target class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): @@ -97,8 +96,8 @@ def get_source_url(self): if self.source == "Nautobot": return None return reverse( - "plugins:nautobot_ssot:sync_add_source", - kwargs={"slug": get_data_source(name=self.source).slug}, + "extras:job", + kwargs={"class_path": self.job_result.name}, ) def get_target_url(self): @@ -106,8 +105,8 @@ def get_target_url(self): if self.target == "Nautobot": return None return reverse( - "plugins:nautobot_ssot:sync_add_target", - kwargs={"slug": get_data_target(name=self.target).slug}, + "extras:job", + kwargs={"class_path": self.job_result.name}, ) diff --git a/nautobot_ssot/sync/__init__.py b/nautobot_ssot/sync/__init__.py deleted file mode 100644 index 14aef5a08..000000000 --- a/nautobot_ssot/sync/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Basic API for data sync sources/targets (workers).""" -import pkg_resources - - -def get_data_sources(): - """Get a list of registered sync worker data sources.""" - # TODO: add option for caching to avoid unnecessary re-loading - return sorted( - [ - entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_sources") - ], - key=lambda worker: worker.name, - ) - - -def get_data_targets(): - """Get a list of registered sync worker data targets.""" - # TODO: add option for caching to avoid unnecessary re-loading - return sorted( - [ - entrypoint.load() for entrypoint in pkg_resources.iter_entry_points("nautobot_ssot.data_targets") - ], - key=lambda worker: worker.name, - ) - - -def get_data_source(name=None, slug=None): - """Look up the specified data source class.""" - # TODO: add option for caching to avoid unnecessary re-loading - for data_source in get_data_sources(): - if name and data_source.name != name: - continue - if slug and data_source.slug != slug: - continue - return data_source - raise KeyError(f'No data source "{name or slug}" found!') - - -def get_data_target(name=None, slug=None): - """Look up the specified data target class.""" - # TODO: add option for caching to avoid unnecessary re-loading - for data_target in get_data_targets(): - if name and data_target.name != name: - continue - if slug and data_target.slug != slug: - continue - return data_target - raise KeyError(f'No data target "{name or slug}" found!') diff --git a/nautobot_ssot/sync/example.py b/nautobot_ssot/sync/example.py deleted file mode 100644 index 37360dad0..000000000 --- a/nautobot_ssot/sync/example.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Example implementation of a Nautobot Data Sync worker class.""" - -from django.db import transaction - -from nautobot.dcim.models import Site -from nautobot.extras.jobs import StringVar -from nautobot.utilities.exceptions import AbortTransaction - -from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices -from nautobot_ssot.sync.worker import DataSyncWorker - - -class ExampleSyncWorker(DataSyncWorker): - - site_slug = StringVar(description="Which site's data to synchronize", default="") - - class Meta: - name = "Example Sync Worker" - slug = "example-sync-worker" - description = "An example of how a sync worker might be implemented." - - def execute(self): - """Perform a mock data synchronization.""" - - # For sake of a simple example, we don't actually use DiffSync here - self.job_log(f"Beginning execution, dry_run = {self.dry_run}") - try: - with transaction.atomic(): - site, created = Site.objects.get_or_create( - slug=self.data["site_slug"], - defaults={"name": self.data["site_slug"]}, - ) - action = SyncLogEntryActionChoices.ACTION_CREATE if created else SyncLogEntryActionChoices.ACTION_UPDATE - self.sync_log( - action=action, - status=SyncLogEntryStatusChoices.STATUS_SUCCESS, - synced_object=site, - ) - if self.dry_run: - # Note that this is not an ideal way to implement dry-run behavior; - # most notably it will also revert any JobResult changes or SyncLogEntry records created above! - raise AbortTransaction() - except AbortTransaction: - self.job_log("Database changes have been reverted automatically.") diff --git a/nautobot_ssot/sync/job.py b/nautobot_ssot/sync/job.py deleted file mode 100644 index 542856bd1..000000000 --- a/nautobot_ssot/sync/job.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Job API for invoking a sync worker.""" -import logging -import traceback - -from django.utils import timezone - -from django_rq import job -import structlog - -from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices - -from nautobot_ssot.choices import SyncLogEntryActionChoices -from nautobot_ssot.models import Sync, SyncLogEntry -from nautobot_ssot.sync import get_data_source, get_data_target - - -logger = logging.getLogger("rq.worker") - - -@job("default") -def sync(sync_id, data): - """Perform a requested sync.""" - sync = Sync.objects.get(id=sync_id) - - sync.job_result.log( - f"START: data synchronization {sync}", grouping="sync", level_choice=LogLevelChoices.LOG_INFO, logger=logger - ) - sync.job_result.set_status(JobResultStatusChoices.STATUS_RUNNING) - sync.job_result.save() - sync.start_time = timezone.now() - sync.save() - - def structlog_to_log_entry(_logger, _log_method, event_dict): - """Capture certain structlog messages from DiffSync into the Nautobot database.""" - if all(key in event_dict for key in ("src", "dst", "action", "model", "unique_id", "diffs", "status")): - sync = event_dict["src"].sync - sync_worker = event_dict["src"].sync_worker - object_repr = event_dict["unique_id"] - # The DiffSync log gives us a model name (string) and unique_id (string). - # Try to look up the actual Nautobot object that this describes. - synced_object, object_change = sync_worker.lookup_object(event_dict["model"], event_dict["unique_id"]) - if synced_object: - object_repr = repr(synced_object) - SyncLogEntry.objects.create( - sync=sync, - action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, - diff=event_dict["diffs"] if event_dict["action"] else None, - status=event_dict["status"], - message=event_dict["event"], - synced_object=synced_object, - object_change=object_change, - object_repr=object_repr, - ) - return event_dict - - try: - structlog.configure( - processors=[ - structlog_to_log_entry, - structlog.stdlib.render_to_log_kwargs, - ], - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - if sync.source != "Nautobot": - sync_worker = get_data_source(name=sync.source)(sync=sync, data=data) - else: - sync_worker = get_data_target(name=sync.target)(sync=sync, data=data) - - sync_worker.execute() - - except Exception as exc: - sync.job_result.log( - f"Exception occurred during {sync}: {exc}", - grouping="sync", - level_choice=LogLevelChoices.LOG_FAILURE, - logger=logger, - ) - logger.error(traceback.format_exc()) - sync.job_result.set_status(JobResultStatusChoices.STATUS_FAILED) - else: - sync.job_result.log( - f"FINISH: data synchronization {sync}", - grouping="sync", - level_choice=LogLevelChoices.LOG_INFO, - logger=logger, - ) - sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) - finally: - sync.job_result.completed = timezone.now() - sync.job_result.save() - - return {"ok": sync.job_result.status == JobResultStatusChoices.STATUS_COMPLETED} diff --git a/nautobot_ssot/sync/worker.py b/nautobot_ssot/sync/worker.py deleted file mode 100644 index 32b233c4d..000000000 --- a/nautobot_ssot/sync/worker.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Base/generic functionality for implementation of a data synchronization worker class.""" -from collections import OrderedDict - -from django.utils.functional import classproperty - -from nautobot.extras.choices import LogLevelChoices -from nautobot.extras.jobs import ScriptVariable - -from nautobot_ssot.forms import SyncForm -from nautobot_ssot.models import SyncLogEntry - - -class DataSyncWorker: - """Semi-abstract base class to serve as a parent for all data sync worker implementations.""" - - # Django: when passed this class or one of its subclasses as context for a template, - # DO NOT automatically instantiate it! - do_not_call_in_templates = True - - def __init__(self, sync=None, data=None): - """Instantiate a DataSyncWorker in preparation for executing the data sync. - - Args: - sync (Sync): Database object that will be used to track the progress of the data sync. - data (dict): Key-value pairs of parameters passed into this worker - """ - self.sync = sync - self.data = data - self.dry_run = data.get("dry_run", True) if data else True - - class Meta: - """Metaclass attributes of a DataSyncWorker. - - Fields that can be defined here, and their default values if undefined, include: - - dry_run_default (True) - Default value for "dry_run" when rendering as a form. - - name (cls.__name__) - Short name of this worker class. - - slug (cls.__name__) - URL-safe unique identifier of this worker class. - - description ("")- Detailed description of this worker class. - """ - - dry_run_default = True - - @classproperty - def name(cls): - """Short name of this worker.""" - return getattr(cls.Meta, "name", cls.__name__) - - @classproperty - def slug(cls): - """URL-safe unique identifier of this worker.""" - return getattr(cls.Meta, "slug", cls.__name__.lower()) - - @classproperty - def description(cls): - """Detailed description of this worker.""" - return getattr(cls.Meta, "description", "") - - @classmethod - def _get_vars(cls): - """Get the dictionary of ScriptVariable attributes on this class.""" - vars = OrderedDict() - for name, attr in cls.__dict__.items(): - if isinstance(attr, ScriptVariable): - vars[name] = attr - return vars - - @classmethod - def as_form(cls, *args, **kwargs): - """Construct a Django form suitable for providing any input parameters required. - - args and kwargs are passed through to the Form constructor. - - Heavily based on nautobot.extras.jobs.Job.as_form(). - """ - fields = {name: var.as_field() for name, var in cls._get_vars().items()} - # Create a new Form class inheriting from SyncForm with fields as its attributes - FormClass = type("SyncForm", (SyncForm,), fields) - # Instantiate the Form class - form = FormClass(*args, **kwargs) - - # Set initial value of "dry run" checkbox - form.fields["dry_run"].initial = getattr(cls.Meta, "dry_run_default", True) - - return form - - @property - def job_result(self): - return self.sync.job_result - - def job_log( - self, - message, - object=None, - level=LogLevelChoices.LOG_DEFAULT, - grouping="sync", - logger=None, - ): - """Log a status message to the JobResult record.""" - self.job_result.log(message, obj=object, level_choice=level, grouping=grouping, logger=logger) - - def sync_log( - self, - action, - status, - message="", - diff=None, - synced_object=None, - object_repr=None, - object_change=None, - ): - """Log a action message as a SyncLogEntry.""" - if not diff: - diff = {} - if synced_object and not object_repr: - object_repr = repr(synced_object) - - SyncLogEntry.objects.create( - sync=self.sync, - action=action, - status=status, - message=message, - diff=diff, - synced_object=synced_object, - object_repr=object_repr, - object_change=object_change, - ) - - # - # Methods to be implemented by subclasses, below: - # - - def execute(self): - """Perform a dry run or actual data synchronization, depending on self.dry_run. - - Note that unlike a Nautobot Job, it is up to the implementation to provide dry-run behavior; - since execution may involve manipulation of data in other systems, Nautobot cannot itself - guarantee proper rollback or other dry-run behavior on its own. - """ - - def lookup_object(self, model_name, unique_id): - """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. - - Optional helper method used to build more detailed/accurate SyncLogEntry records from DiffSync logs. - - Args: - model_name (str): DiffSyncModel class name or similar class/model label. - unique_id (str): DiffSyncModel unique_id or similar unique identifier. - - Returns: - tuple: (nautobot_record, nautobot_objectchange_record). Either or both may be None. - """ - return (None, None) diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 7b1682928..28b374d05 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -5,7 +5,6 @@ from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices from .models import Sync, SyncLogEntry -from .sync import get_data_source, get_data_target ACTION_LOGS_LINK = """ @@ -55,6 +54,8 @@ class SyncTable(BaseTable): """Table for listing Sync records.""" pk = ToggleColumn() + source = Column(linkify=lambda record: record.get_source_url()) + target = Column(linkify=lambda record: record.get_target_url()) start_time = DateTimeColumn(linkify=True, short=True) duration = TemplateColumn(template_code="{% load shorter_timedelta %}{{ record.duration | shorter_timedelta }}") dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") diff --git a/nautobot_ssot/template_content.py b/nautobot_ssot/template_content.py new file mode 100644 index 000000000..5ae023fb2 --- /dev/null +++ b/nautobot_ssot/template_content.py @@ -0,0 +1,26 @@ +from django.urls import reverse + +from nautobot.extras.plugins import PluginTemplateExtension + +from nautobot_ssot.models import Sync + + +class JobResultSyncLink(PluginTemplateExtension): + """Add button linking to Sync data for relevant JobResults.""" + + model = "extras.jobresult" + + def buttons(self): + try: + sync = Sync.objects.get(job_result=self.context["object"]) + return f""" + + """ + except Sync.DoesNotExist: + return "" + +template_extensions = [JobResultSyncLink] diff --git a/nautobot_ssot/templates/nautobot_ssot/dashboard.html b/nautobot_ssot/templates/nautobot_ssot/dashboard.html index 1a6898550..bc197e68c 100644 --- a/nautobot_ssot/templates/nautobot_ssot/dashboard.html +++ b/nautobot_ssot/templates/nautobot_ssot/dashboard.html @@ -21,7 +21,7 @@

{% block title %}Single Source of Truth{% endblock %}

{% dashboard_data data_source queryset "source" %}

- + {{ data_source.name }}

@@ -45,7 +45,7 @@

{% dashboard_data data_target queryset "target" %}

- + {{ data_target.name }}

diff --git a/nautobot_ssot/urls.py b/nautobot_ssot/urls.py index 11921e620..fbf65f504 100644 --- a/nautobot_ssot/urls.py +++ b/nautobot_ssot/urls.py @@ -6,18 +6,6 @@ urlpatterns = [ path("", views.DashboardView.as_view(), name="dashboard"), - path( - "sync/to//", - views.SyncCreateView.as_view(), - name="sync_add_target", - kwargs={"kind": "target"}, - ), - path( - "sync/from//", - views.SyncCreateView.as_view(), - name="sync_add_source", - kwargs={"kind": "source"}, - ), path("history/", views.SyncListView.as_view(), name="sync_list"), path("history/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), path("history//", views.SyncView.as_view(), name="sync"), diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index c08585fe4..ecce220de 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -18,24 +18,25 @@ from .filters import SyncFilter, SyncLogEntryFilter from .forms import SyncFilterForm, SyncLogEntryFilterForm +from .jobs import get_data_jobs from .models import Sync, SyncLogEntry -from .sync import get_data_sources, get_data_targets, get_data_source, get_data_target from .tables import DashboardTable, SyncTable, SyncLogEntryTable class DashboardView(ObjectListView): """Dashboard / overview of SSoT.""" - queryset = Sync.queryset() + queryset = Sync.objects.all() table = DashboardTable action_buttons = [] template_name = "nautobot_ssot/dashboard.html" def extra_context(self): + data_sources, data_targets = get_data_jobs() context = { "queryset": self.queryset, - "data_sources": get_data_sources(), - "data_targets": get_data_targets(), + "data_sources": data_sources, + "data_targets": data_targets, "source": {}, "target": {}, } @@ -43,12 +44,12 @@ def extra_context(self): for source in context["data_sources"]: context["source"][source.name] = self.queryset.filter( job_result__obj_type=sync_ct, - job_result__name=source.name, + job_result__name=source.class_path, ) for target in context["data_targets"]: context["target"][target.name] = self.queryset.filter( job_result__obj_type=sync_ct, - job_result__name=target.name, + job_result__name=target.class_path, ) return context @@ -64,89 +65,13 @@ class SyncListView(ObjectListView): template_name = "nautobot_ssot/history.html" def extra_context(self): + data_sources, data_targets = get_data_jobs() return { - "data_sources": get_data_sources(), - "data_targets": get_data_targets(), + "data_sources": data_sources, + "data_targets": data_targets, } -class SyncCreateView(ObjectEditView): - """View for starting a new Sync.""" - - queryset = Sync.objects.all() - - def get(self, request, slug, kind="source"): - """Render a form for executing the given sync worker.""" - - try: - if kind == "source": - sync_worker_class = get_data_source(slug=slug) - else: - sync_worker_class = get_data_target(slug=slug) - except KeyError: - raise Http404 - - form = sync_worker_class.as_form(initial=request.GET) - - return render( - request, - "nautobot_ssot/sync_run.html", - { - "sync_worker_class": sync_worker_class, - "form": form, - }, - ) - - def post(self, request, slug, kind="source"): - """Enqueue the given sync worker for execution!""" - try: - if kind == "source": - sync_worker_class = get_data_source(slug=slug) - source = sync_worker_class.name - target = "Nautobot" - else: - sync_worker_class = get_data_target(slug=slug) - source = "Nautobot" - target = sync_worker_class.name - except KeyError: - raise Http404 - - if not Worker.count(get_connection("default")): - messages.error(request, "Unable to perform sync: RQ worker process not running.") - - form = sync_worker_class.as_form(request.POST, request.FILES) - - if form.is_valid(): - dry_run = form.cleaned_data.get("dry_run", True) - - sync = Sync.objects.create(source=source, target=target, dry_run=dry_run, diff={}) - job_result = JobResult.objects.create( - name=sync_worker_class.name, - obj_type=ContentType.objects.get_for_model(sync), - user=request.user, - job_id=sync.pk, - ) - sync.job_result = job_result - sync.save() - - transaction.on_commit( - lambda: get_queue("default").enqueue( - "nautobot_ssot.sync.job.sync", sync_id=sync.pk, data=form.cleaned_data, job_timeout=3600 - ) - ) - - return redirect("plugins:nautobot_ssot:sync", pk=sync.pk) - - return render( - request, - "nautobot_ssot/sync_run.html", - { - "sync_worker_class": sync_worker_class, - "form": form, - }, - ) - - class SyncDeleteView(ObjectDeleteView): """View for deleting a single Sync record.""" From 1bcf8f2ca41d7e7a6456b021911b13c0dcdd7d70 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 15 Jun 2021 16:37:02 -0400 Subject: [PATCH 15/42] Temporarily add dependency on my PR branch --- poetry.lock | 84 ++++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/poetry.lock b/poetry.lock index 201e5c1eb..7ee70c5df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -226,7 +226,7 @@ structlog = ">=20.1.0,<21.0.0" [[package]] name = "django" -version = "3.1.11" +version = "3.1.12" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -807,41 +807,48 @@ python-versions = "*" [[package]] name = "nautobot" -version = "1.0.2" +version = "1.0.3-beta.1" description = "Source of truth and network automation platform." category = "main" optional = false -python-versions = ">=3.6,<4.0" - -[package.dependencies] -Django = ">=3.1.8,<3.2.0" -django-cacheops = ">=5.1,<5.2" -django-cors-headers = ">=3.7.0,<3.8.0" -django-cryptography = ">=1.0,<1.1" -django-filter = ">=2.4.0,<2.5.0" -django-mptt = ">=0.11.0,<0.12.0" -django-prometheus = ">=2.1.0,<2.2.0" -django-redis = ">=4.12.1,<4.13.0" -django-rq = ">=2.4.1,<2.5.0" -django-tables2 = ">=2.3.4,<2.4.0" -django-taggit = ">=1.3.0,<1.4.0" -django-timezone-field = ">=4.1.2,<4.2.0" -django-webserver = ">=1.2.0,<1.3.0" -djangorestframework = ">=3.12.4,<3.13.0" -drf-yasg = {version = ">=1.20.0,<1.21.0", extras = ["validation"]} -GitPython = ">=3.1.15,<3.2.0" -graphene-django = ">=2.15.0,<2.16.0" -importlib-metadata = {version = ">=3.4.0,<3.5.0", markers = "python_version < \"3.8\""} -Jinja2 = ">=2.11.3,<2.12.0" -Markdown = ">=3.3.4,<3.4.0" -netaddr = ">=0.8.0,<0.9.0" -Pillow = ">=8.1.0,<8.2.0" -psycopg2-binary = ">=2.8.6,<2.9.0" -pycryptodome = ">=3.10.1,<3.11.0" -pyuwsgi = ">=2.0.19.1.post0,<2.1.0.0" -PyYAML = ">=5.4.1,<5.5.0" -social-auth-app-django = ">=4.0.0,<5.0.0" -svgwrite = ">=1.4.1,<1.5.0" +python-versions = "^3.6" +develop = false + +[package.dependencies] +Django = "~3.1.12" +django-cacheops = "~5.1" +django-cors-headers = "~3.7.0" +django-cryptography = "~1.0" +django-filter = "~2.4.0" +django-mptt = "~0.11.0" +django-prometheus = "~2.1.0" +django-redis = "~4.12.1" +django-rq = "~2.4.1" +django-tables2 = "~2.3.4" +django-taggit = "~1.3.0" +django-timezone-field = "~4.1.2" +django-webserver = "~1.2.0" +djangorestframework = "~3.12.4" +drf-yasg = {version = "~1.20.0", extras = ["validation"]} +GitPython = "~3.1.15" +graphene-django = "~2.15.0" +importlib-metadata = {version = "~3.4.0", markers = "python_version < \"3.8\""} +Jinja2 = "~2.11.3" +Markdown = "~3.3.4" +netaddr = "~0.8.0" +Pillow = "~8.1.0" +psycopg2-binary = "~2.8.6" +pycryptodome = "~3.10.1" +pyuwsgi = "~2.0.19.1.post0" +PyYAML = "~5.4.1" +social-auth-app-django = "^4.0.0" +svgwrite = "~1.4.1" + +[package.source] +type = "git" +url = "https://github.com/nautobot/nautobot.git" +reference = "gfm-jobresult-plugintemplate" +resolved_reference = "3dde6019eb59da4d61bf85fbfb7fe8f1f94125fb" [[package]] name = "netaddr" @@ -1463,7 +1470,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "8ec824bea06391a19886eb095cc8295b5b261cdbed3733458b373be19b5d5332" +content-hash = "b31f18f65590b6cf0621e61d1d914d06102eaed53b9e647132ed05d6832628a6" [metadata.files] aniso8601 = [ @@ -1637,8 +1644,8 @@ diffsync = [ {file = "diffsync-1.3.0.tar.gz", hash = "sha256:ab5499293e307f872056a757bea22e0c88ec91e88dfdba4d6bdc59832835b2af"}, ] django = [ - {file = "Django-3.1.11-py3-none-any.whl", hash = "sha256:c79245c488411d1ae300b8f7a08ac18a496380204cf3035aff97ad917a8de999"}, - {file = "Django-3.1.11.tar.gz", hash = "sha256:9a0a2f3d34c53032578b54db7ec55929b87dda6fec27a06cc2587afbea1965e5"}, + {file = "Django-3.1.12-py3-none-any.whl", hash = "sha256:a523d62b7ab2908f551dabc32b99017a86aa7784e32b761708e52be3dce6d35d"}, + {file = "Django-3.1.12.tar.gz", hash = "sha256:dc41bf07357f1f4810c1c555b685cb51f780b41e37892d6cc92b89789f2847e1"}, ] django-appconf = [ {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"}, @@ -1870,10 +1877,7 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -nautobot = [ - {file = "nautobot-1.0.2-py3-none-any.whl", hash = "sha256:319ca74089a9b77829d559c2a8e8151aebb309c53a54cd08cb298bd573afbbdf"}, - {file = "nautobot-1.0.2.tar.gz", hash = "sha256:a6d3ba1f8d87267e05035067a224d6a3c0e2d150f19219ee2691837f28ed7189"}, -] +nautobot = [] netaddr = [ {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"}, {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"}, diff --git a/pyproject.toml b/pyproject.toml index 8e356bed9..3db181732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.6" -nautobot = "^1.0.2" +nautobot = {git = "https://github.com/nautobot/nautobot.git", rev = "gfm-jobresult-plugintemplate"} diffsync = "^1.3.0" [tool.poetry.dev-dependencies] From 3b9a5de4947f03727b84accbe0a12b03cbabd391 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 15 Jun 2021 17:48:56 -0400 Subject: [PATCH 16/42] Initial very rough version of data_source_target view --- nautobot_ssot/jobs/base.py | 25 +++++++++- .../templates/nautobot_ssot/dashboard.html | 10 +++- .../nautobot_ssot/data_source_target.html | 46 +++++++++++++++++++ nautobot_ssot/urls.py | 2 + nautobot_ssot/views.py | 40 ++++++++++++---- 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 nautobot_ssot/templates/nautobot_ssot/data_source_target.html diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index d314c6a77..ec1fdb4e6 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -1,6 +1,7 @@ """Base Job classes for sync workers.""" from django.forms import HiddenInput +from django.templatetags.static import static from django.utils import timezone import structlog @@ -17,8 +18,10 @@ class DataSyncBaseJob(BaseJob): Works mostly as per the BaseJob API, with the following changes: - Concrete subclasses are responsible for implementing `self.sync_data()`, **not** `self.run()`. - - Additional Meta field `dry_run_default` is available to be defined if desired. - - Meta fields `data_source` and `data_target` can be defined for lookup purposes + - Subclasses may optionally define any Meta field supported by Jobs, as well as the following: + - `dry_run_default` - defaults to True if unspecified + - `data_source` and `data_target` as labels (by default, will use the `name` and/or "Nautobot" as appropriate) + - `data_source_icon` and `data_target_icon` """ dry_run = BooleanVar() @@ -121,6 +124,16 @@ def data_target(self): """The system or data source being modified by this sync.""" return getattr(self.Meta, "data_target", self.name) + @property + def data_source_icon(self): + """Icon corresponding to the data_source.""" + return getattr(self.Meta, "data_source_icon", None) + + @property + def data_target_icon(self): + """Icon corresponding to the data_target.""" + return getattr(self.Meta, "data_target_icon", None) + def run(self, data, commit): """Job entry point from Nautobot - do not override!""" self.sync = Sync.objects.create( @@ -156,6 +169,10 @@ class DataSource(DataSyncBaseJob): def data_target(self): return "Nautobot" + @property + def data_target_icon(self): + return static("img/nautobot_icon_384x384.png") + class DataTarget(DataSyncBaseJob): """Base class for Jobs that sync data **to** another data target **from** Nautobot.""" @@ -165,3 +182,7 @@ class DataTarget(DataSyncBaseJob): @property def data_source(self): return "Nautobot" + + @property + def data_source_icon(self): + return static("img/nautobot_icon_384x384.png") diff --git a/nautobot_ssot/templates/nautobot_ssot/dashboard.html b/nautobot_ssot/templates/nautobot_ssot/dashboard.html index bc197e68c..3ab66767b 100644 --- a/nautobot_ssot/templates/nautobot_ssot/dashboard.html +++ b/nautobot_ssot/templates/nautobot_ssot/dashboard.html @@ -19,9 +19,12 @@

{% block title %}Single Source of Truth{% endblock %}

{% dashboard_data data_source queryset "source" %} + + Sync Now +

- + {{ data_source.name }}

@@ -43,9 +46,12 @@

{% dashboard_data data_target queryset "target" %} + + Sync Now +

- + {{ data_target.name }}

diff --git a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html new file mode 100644 index 000000000..b9fdeb2e9 --- /dev/null +++ b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% load helpers %} + +{% block header %} +
+
+ +
+
+

{% block title %}SSoT - {{ job_class }}{% endblock %}

+{% endblock %} + +{% block content %} +
+
+ + + + + + + + + + + +
+ {% if job_class.data_source_icon %} + + {% else %} + + {% endif %} +

+ {% if job_class.data_target_icon %} + + {% else %} + + {% endif %} +

{{ job_class.data_source }}

{{ job_class.data_target }}

+
+
+{% endblock %} diff --git a/nautobot_ssot/urls.py b/nautobot_ssot/urls.py index fbf65f504..863332483 100644 --- a/nautobot_ssot/urls.py +++ b/nautobot_ssot/urls.py @@ -6,6 +6,8 @@ urlpatterns = [ path("", views.DashboardView.as_view(), name="dashboard"), + path("data-sources//", views.DataSourceTargetView.as_view(), name="data_source"), + path("data-targets//", views.DataSourceTargetView.as_view(), name="data_target"), path("history/", views.SyncListView.as_view(), name="sync_list"), path("history/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), path("history//", views.SyncView.as_view(), name="sync"), diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index ecce220de..ea7668cce 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -3,22 +3,18 @@ import pprint from django.contrib.contenttypes.models import ContentType -from django.contrib import messages -from django.db import transaction from django.http import Http404 -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render +from django.views.generic import View -from django_rq import get_queue -from django_rq.queues import get_connection -from rq import Worker - -from nautobot.extras.models import JobResult +from nautobot.extras.jobs import get_job from nautobot.extras.views import ObjectChangeLogView -from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectView +from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectListView, ObjectView +from nautobot.utilities.views import ContentTypePermissionRequiredMixin from .filters import SyncFilter, SyncLogEntryFilter from .forms import SyncFilterForm, SyncLogEntryFilterForm -from .jobs import get_data_jobs +from .jobs import get_data_jobs, DataSource, DataTarget from .models import Sync, SyncLogEntry from .tables import DashboardTable, SyncTable, SyncLogEntryTable @@ -54,6 +50,30 @@ def extra_context(self): return context + +class DataSourceTargetView(ContentTypePermissionRequiredMixin, View): + """Detail view of a given Data Source or Data Target Job.""" + + def get_required_permission(self): + return "extras.view_job" + + def get(self, request, class_path): + job_class = get_job(class_path) + if not job_class or not issubclass(job_class, (DataSource, DataTarget)): + raise Http404 + + syncs = Sync.objects.filter(source=job_class.data_source, target=job_class.data_target) + + return render( + request, + "nautobot_ssot/data_source_target.html", + { + "job_class": job_class, + "syncs": syncs, + "source_or_target": "source" if issubclass(job_class, DataSource) else "target", + }, + ) + class SyncListView(ObjectListView): """View for listing Sync records.""" From 81b61c93e8a8750f692c2731630dded592d020db Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 16 Jun 2021 15:33:28 -0400 Subject: [PATCH 17/42] Refine per-data-source/target detail view --- nautobot_ssot/jobs/base.py | 54 +++++++++------ nautobot_ssot/jobs/examples.py | 16 ++++- nautobot_ssot/tables.py | 17 +++++ .../nautobot_ssot/data_source_target.html | 68 +++++++++++++++++-- .../templates/nautobot_ssot/sync_header.html | 15 ++++ nautobot_ssot/views.py | 7 +- 6 files changed, 146 insertions(+), 31 deletions(-) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index ec1fdb4e6..b3b3d1a02 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -1,8 +1,10 @@ """Base Job classes for sync workers.""" +from collections import namedtuple from django.forms import HiddenInput from django.templatetags.static import static from django.utils import timezone +from django.utils.functional import classproperty import structlog @@ -12,6 +14,9 @@ from nautobot_ssot.models import Sync, SyncLogEntry +DataMapping = namedtuple("DataMapping", ["source_name", "source_url", "target_name", "target_url"]) + + class DataSyncBaseJob(BaseJob): """Common base class for data synchronization jobs. @@ -52,6 +57,15 @@ def lookup_object(self, model_name, unique_id): """ return (None, None) + @classmethod + def data_mappings(cls): + """List the data mappings involved in this sync job. + + Returns: + Iterable[DataMapping] + """ + return [] + def sync_log( self, action, @@ -114,25 +128,25 @@ def as_form(self, data=None, files=None, initial=None): form.fields["_commit"].widget = HiddenInput() return form - @property - def data_source(self): + @classproperty + def data_source(cls): """The system or data source providing input data for this sync.""" - return getattr(self.Meta, "data_source", self.name) + return getattr(cls.Meta, "data_source", cls.name) - @property - def data_target(self): + @classproperty + def data_target(cls): """The system or data source being modified by this sync.""" - return getattr(self.Meta, "data_target", self.name) + return getattr(cls.Meta, "data_target", cls.name) - @property - def data_source_icon(self): + @classproperty + def data_source_icon(cls): """Icon corresponding to the data_source.""" - return getattr(self.Meta, "data_source_icon", None) + return getattr(cls.Meta, "data_source_icon", None) - @property - def data_target_icon(self): + @classproperty + def data_target_icon(cls): """Icon corresponding to the data_target.""" - return getattr(self.Meta, "data_target_icon", None) + return getattr(cls.Meta, "data_target_icon", None) def run(self, data, commit): """Job entry point from Nautobot - do not override!""" @@ -165,12 +179,12 @@ class DataSource(DataSyncBaseJob): dry_run = BooleanVar(description="Perform a dry-run, making no actual changes to Nautobot data.") - @property - def data_target(self): + @classproperty + def data_target(cls): return "Nautobot" - @property - def data_target_icon(self): + @classproperty + def data_target_icon(cls): return static("img/nautobot_icon_384x384.png") @@ -179,10 +193,10 @@ class DataTarget(DataSyncBaseJob): dry_run = BooleanVar(description="Perform a dry-run, making no actual changes to the remote system.") - @property - def data_source(self): + @classproperty + def data_source(cls): return "Nautobot" - @property - def data_source_icon(self): + @classproperty + def data_source_icon(cls): return static("img/nautobot_icon_384x384.png") diff --git a/nautobot_ssot/jobs/examples.py b/nautobot_ssot/jobs/examples.py index 01205b096..f4919ada1 100644 --- a/nautobot_ssot/jobs/examples.py +++ b/nautobot_ssot/jobs/examples.py @@ -1,8 +1,10 @@ +from django.urls import reverse + from nautobot.dcim.models import Site from nautobot.extras.jobs import StringVar, Job from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices -from nautobot_ssot.jobs.base import DataSource, DataTarget +from nautobot_ssot.jobs.base import DataMapping, DataSource, DataTarget class ExampleDataSource(DataSource, Job): @@ -15,6 +17,12 @@ class Meta: description = "An example of a 'data source' Job for loading data into Nautobot from elsewhere." data_source = "Dummy Data" + @classmethod + def data_mappings(cls): + return ( + DataMapping("site slug", None, "Site", reverse("dcim:site_list")), + ) + def sync_data(self): """Perform data sync into Nautobot.""" # For sake of a simple example, we don't actually use DiffSync here. @@ -40,6 +48,12 @@ class Meta: description = "An example of a 'data target' Job for loading data from Nautobot into elsewhere." data_target = "Dummy Data" + @classmethod + def data_mappings(cls): + return ( + DataMapping("Site", reverse("dcim:site_list"), "site slug", None), + ) + def sync_data(self): """Perform data sync from Nautobot.""" # For sake of a simple example, we don't actually use DiffSync here. diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 28b374d05..0189c8dc1 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -133,6 +133,23 @@ class Meta(BaseTable.Meta): order_by = ("-start_time",) +class SyncTableSingleSourceOrTarget(SyncTable): + """Subclass of SyncTable with fewer default columns.""" + + class Meta(SyncTable.Meta): + default_columns = ( + "start_time", + "status", + "dry_run", + "num_unchanged", + "num_created", + "num_updated", + "num_deleted", + "num_failed", + "num_errored", + ) + + ACTION_LABEL = """{{ record.action }}""" diff --git a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html index b9fdeb2e9..97408f587 100644 --- a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html +++ b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load helpers %} +{% load render_table from django_tables2 %} {% block header %}
@@ -11,36 +12,89 @@
+

{% block title %}SSoT - {{ job_class }}{% endblock %}

+ {% endblock %} {% block content %}
-
+
- - - + - +
+ {% if job_class.data_source_icon %} - + {% else %} {% endif %}

+ {% if job_class.data_target_icon %} - + {% else %} {% endif %}

{{ job_class.data_source }}

{{ job_class.data_source }}

{{ job_class.data_target }}

{{ job_class.data_target }}

+
+
+
Data Mappings
+ + + + + + {% for source_name, source_url, target_name, target_url in job_class.data_mappings %} + + + + + {% empty %} + + + + + {% endfor %} +
{{ job_class.data_source }}{{ job_class.data_target }}
+ {% if source_url %} + {{ source_name }} + {% else %} + {{ source_name }} + {% endif %} + + {% if target_url %} + {{ target_name }} + {% else %} + {{ target_name }} + {% endif %} +
+
+
+
+
+
+
+
Sync History
+ {% render_table table 'inc/table.html' %} +
+
{% endblock %} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_header.html b/nautobot_ssot/templates/nautobot_ssot/sync_header.html index 461b0935f..69fc62a84 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_header.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_header.html @@ -8,6 +8,21 @@
diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index ea7668cce..564b732f1 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -16,7 +16,7 @@ from .forms import SyncFilterForm, SyncLogEntryFilterForm from .jobs import get_data_jobs, DataSource, DataTarget from .models import Sync, SyncLogEntry -from .tables import DashboardTable, SyncTable, SyncLogEntryTable +from .tables import DashboardTable, SyncTable, SyncTableSingleSourceOrTarget, SyncLogEntryTable class DashboardView(ObjectListView): @@ -62,14 +62,15 @@ def get(self, request, class_path): if not job_class or not issubclass(job_class, (DataSource, DataTarget)): raise Http404 - syncs = Sync.objects.filter(source=job_class.data_source, target=job_class.data_target) + syncs = Sync.queryset().filter(source=job_class.data_source, target=job_class.data_target) + table = SyncTableSingleSourceOrTarget(syncs, user=request.user) return render( request, "nautobot_ssot/data_source_target.html", { "job_class": job_class, - "syncs": syncs, + "table": table, "source_or_target": "source" if issubclass(job_class, DataSource) else "target", }, ) From ec0e365a74cf9c0c3583d2cd3653c62ec31c1a5f Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 17 Jun 2021 11:25:00 -0400 Subject: [PATCH 18/42] Fix bug with null object_repr, refine UI --- nautobot_ssot/jobs/base.py | 6 +++--- .../migrations/0003_object_repr_blank.py | 18 ++++++++++++++++++ nautobot_ssot/models.py | 2 +- nautobot_ssot/tables.py | 4 ++-- .../templates/nautobot_ssot/dashboard.html | 16 ++++++++++------ .../nautobot_ssot/data_source_target.html | 7 +++---- 6 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 nautobot_ssot/migrations/0003_object_repr_blank.py diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index b3b3d1a02..dc51e709e 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -73,7 +73,7 @@ def sync_log( message="", diff=None, synced_object=None, - object_repr=None, + object_repr="", object_change=None, ): """Log a action message as a SyncLogEntry.""" @@ -185,7 +185,7 @@ def data_target(cls): @classproperty def data_target_icon(cls): - return static("img/nautobot_icon_384x384.png") + return static("img/nautobot_logo.png") class DataTarget(DataSyncBaseJob): @@ -199,4 +199,4 @@ def data_source(cls): @classproperty def data_source_icon(cls): - return static("img/nautobot_icon_384x384.png") + return static("img/nautobot_logo.png") diff --git a/nautobot_ssot/migrations/0003_object_repr_blank.py b/nautobot_ssot/migrations/0003_object_repr_blank.py new file mode 100644 index 000000000..7eda814eb --- /dev/null +++ b/nautobot_ssot/migrations/0003_object_repr_blank.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.12 on 2021-06-17 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nautobot_ssot', '0002_refinements'), + ] + + operations = [ + migrations.AlterField( + model_name='synclogentry', + name='object_repr', + field=models.CharField(blank=True, default='', editable=False, max_length=200), + ), + ] diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index f635a54b1..c3a130793 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -141,7 +141,7 @@ class SyncLogEntry(BaseModel): synced_object_id = models.UUIDField(blank=True, null=True) synced_object = GenericForeignKey(ct_field="synced_object_type", fk_field="synced_object_id") - object_repr = models.CharField(max_length=200, editable=False) + object_repr = models.CharField(max_length=200, blank=True, default="", editable=False) object_change = models.ForeignKey(to=ObjectChange, on_delete=models.SET_NULL, blank=True, null=True) message = models.CharField(max_length=511, blank=True) diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 0189c8dc1..6e4d10323 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -42,7 +42,7 @@ class DashboardTable(BaseTable): source = Column(linkify=lambda record: record.get_source_url()) target = Column(linkify=lambda record: record.get_target_url()) status = TemplateColumn(template_code="{% include 'extras/inc/job_label.html' with result=record.job_result %}") - dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") + dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Type") class Meta(BaseTable.Meta): model = Sync @@ -58,7 +58,7 @@ class SyncTable(BaseTable): target = Column(linkify=lambda record: record.get_target_url()) start_time = DateTimeColumn(linkify=True, short=True) duration = TemplateColumn(template_code="{% load shorter_timedelta %}{{ record.duration | shorter_timedelta }}") - dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Sync?") + dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Type") status = TemplateColumn(template_code="{% include 'extras/inc/job_label.html' with result=record.job_result %}") num_unchanged = TemplateColumn( diff --git a/nautobot_ssot/templates/nautobot_ssot/dashboard.html b/nautobot_ssot/templates/nautobot_ssot/dashboard.html index 3ab66767b..de1822fc4 100644 --- a/nautobot_ssot/templates/nautobot_ssot/dashboard.html +++ b/nautobot_ssot/templates/nautobot_ssot/dashboard.html @@ -19,15 +19,17 @@

{% block title %}Single Source of Truth{% endblock %}

{% dashboard_data data_source queryset "source" %} - - Sync Now -

{{ data_source.name }}

+ + + Sync + +

{{ data_source.description }}

{% endfor %} @@ -46,15 +48,17 @@

{% dashboard_data data_target queryset "target" %} - - Sync Now -

{{ data_target.name }}

+ + + Sync + +

{{ data_target.description }}

{% endfor %} diff --git a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html index 97408f587..1ffee4b9e 100644 --- a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html +++ b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html @@ -89,12 +89,11 @@

{% block title %}SSoT - {{ job_class }}{% endblock %}

+
-
-
Sync History
- {% render_table table 'inc/table.html' %} -
+

Sync History

+ {% render_table table 'inc/table.html' %}
{% endblock %} From c1d0ea58108caf0bf3620e348c63cc4e032e2546 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 17 Jun 2021 16:58:10 -0400 Subject: [PATCH 19/42] Improved logging; a few UI tweaks in progress --- nautobot_ssot/jobs/base.py | 11 +++++++++++ .../nautobot_ssot/data_source_target.html | 17 +++++++++++++++++ .../templates/nautobot_ssot/sync_detail.html | 13 +++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index dc51e709e..1319af283 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -66,6 +66,15 @@ def data_mappings(cls): """ return [] + @classmethod + def config_information(cls): + """Return a dict of user-facing configuration information {property: value}. + + Note that this will be rendered 'as-is' in the UI, so as a general practice this + should NOT include sensitive information such as passwords! + """ + return {} + def sync_log( self, action, @@ -97,12 +106,14 @@ def _structlog_to_sync_log_entry(self, _logger, _log_method, event_dict): # The DiffSync log gives us a model name (string) and unique_id (string). # Try to look up the actual Nautobot object that this describes. synced_object, object_change = self.lookup_object(event_dict["model"], event_dict["unique_id"]) + object_repr = repr(synced_object) if synced_object else f"{event_dict['model']} {event_dict['unique_id']}" self.sync_log( action=event_dict["action"] or SyncLogEntryActionChoices.ACTION_NO_CHANGE, diff=event_dict["diffs"] if event_dict["action"] else None, status=event_dict["status"], message=event_dict["event"], synced_object=synced_object, + object_repr=object_repr, object_change=object_change, ) diff --git a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html index 1ffee4b9e..c4f266268 100644 --- a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html +++ b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html @@ -53,6 +53,23 @@

{% block title %}SSoT - {{ job_class }}{% endblock %}

{{ job_class.data_target }}

+
+
Configuration
+ + + + + + {% for parameter, value in job_class.config_information.items %} + + + + + {% endfor %} +
ParameterValue
{{ parameter }} + {% if value %}{{ value }}{% else %}—{% endif %} +
+
diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index beef934f3..cd737401d 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -32,7 +32,7 @@ - Sync or Dry run? + Type {% if object.dry_run %} Dry Run @@ -63,6 +63,9 @@
+ {% plugin_left_page object %} +
+
Statistics @@ -103,16 +106,18 @@
{% include 'inc/custom_fields_panel.html' %} {% include 'inc/relationships_panel.html' %} - {% plugin_left_page object %} + {% plugin_right_page object %}
-
+
+
+
Diff
{{ diff }}
- {% plugin_right_page object %}
+ {% plugin_full_width_page object %}
{% endblock %} From 7e2e03cfb0c2e74507eb31db9813ed3b9666784e Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 09:57:07 -0400 Subject: [PATCH 20/42] Update temporary Nautobot dependency now that nautobot/nautobot#576 is merged --- poetry.lock | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7ee70c5df..18722296d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -847,8 +847,8 @@ svgwrite = "~1.4.1" [package.source] type = "git" url = "https://github.com/nautobot/nautobot.git" -reference = "gfm-jobresult-plugintemplate" -resolved_reference = "3dde6019eb59da4d61bf85fbfb7fe8f1f94125fb" +reference = "develop" +resolved_reference = "83ff581ee0bc27c08f79197e0bb727d5262c3db3" [[package]] name = "netaddr" @@ -1470,7 +1470,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "b31f18f65590b6cf0621e61d1d914d06102eaed53b9e647132ed05d6832628a6" +content-hash = "c1dcedb76d6a8cfae48860df80c80983c5c44bba001183d018ed47535275dc77" [metadata.files] aniso8601 = [ diff --git a/pyproject.toml b/pyproject.toml index 3db181732..d37c0bccf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.6" -nautobot = {git = "https://github.com/nautobot/nautobot.git", rev = "gfm-jobresult-plugintemplate"} +nautobot = {git = "https://github.com/nautobot/nautobot.git", rev = "develop"} diffsync = "^1.3.0" [tool.poetry.dev-dependencies] From db5bcf3b4a15c2783feecea0a34db71219446a9c Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 11:44:39 -0400 Subject: [PATCH 21/42] Combine synced_object and object_repr columns into one --- nautobot_ssot/tables.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 6e4d10323..536321045 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -156,6 +156,15 @@ class Meta(SyncTable.Meta): LOG_STATUS_LABEL = """{{ record.status }}""" +SYNCED_OBJECT = """ +{% if record.synced_object %} +{{ record.synced_object}} +{% else %} +{{ record.object_repr }} +{% endif %} +""" + + class SyncLogEntryTable(BaseTable): """Table for displaying SyncLogEntry records.""" @@ -165,11 +174,11 @@ class SyncLogEntryTable(BaseTable): status = TemplateColumn(template_code=LOG_STATUS_LABEL) diff = JSONColumn(orderable=False) message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) - synced_object = LinkColumn(verbose_name="Synced Object") + synced_object = TemplateColumn(template_code=SYNCED_OBJECT) object_change = LinkColumn() class Meta(BaseTable.Meta): model = SyncLogEntry - fields = ("pk", "timestamp", "sync", "action", "synced_object_type", "synced_object", "object_repr", "object_change", "status", "diff", "message") + fields = ("pk", "timestamp", "sync", "action", "synced_object_type", "synced_object", "object_change", "status", "diff", "message") default_columns = ("pk", "timestamp", "sync", "action", "synced_object", "status", "diff", "message") order_by = ("-timestamp",) From 1ec71ab39f7dfe3c96195ad873a3d68a6ca0f3b7 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 12:33:24 -0400 Subject: [PATCH 22/42] Diff rendering, first version --- .../templates/nautobot_ssot/sync_detail.html | 11 ++++- nautobot_ssot/templatetags/render_diff.py | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 nautobot_ssot/templatetags/render_diff.py diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index cd737401d..b64941aed 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -2,8 +2,15 @@ {% load buttons %} {% load plugins %} {% load shorter_timedelta %} +{% load render_diff %} {% block content %} +
@@ -115,7 +122,9 @@
Diff
-
{{ diff }}
+
+ {% render_diff object.diff %} +
{% plugin_full_width_page object %} diff --git a/nautobot_ssot/templatetags/render_diff.py b/nautobot_ssot/templatetags/render_diff.py new file mode 100644 index 000000000..1a60e0cef --- /dev/null +++ b/nautobot_ssot/templatetags/render_diff.py @@ -0,0 +1,43 @@ +from django import template +from django.utils.safestring import mark_safe + + +register = template.Library() + + +def render_diff_recursive(diff): + result = "" + for type, children in diff.items(): + child_result = "" + for child, child_diffs in children.items(): + if "+" in child_diffs and "-" not in child_diffs: + child_class = "diff-added" + elif "-" in child_diffs and "+" not in child_diffs: + child_class = "diff-subtracted" + elif not child_diffs.get("+") and not child_diffs.get("-"): + child_class = "diff-unchanged" + else: + child_class = "diff-changed" + child_result += f'
  • {child}
      ' + + for attr, value in child_diffs.pop("+", {}).items(): + child_result += f'
    • {attr}: {value}
    • ' + + for attr, value in child_diffs.pop("-", {}).items(): + child_result += f'
    • {attr}: {value}
    • ' + + if child_diffs: + child_result += render_diff_recursive(child_diffs) + + child_result += f"
  • " + child_result = f"
      {child_result}
    " + result += f"
  • {type}{child_result}
  • " + return result + + +@register.simple_tag +def render_diff(diff): + """Render a DiffSync diff dict to HTML.""" + result = f"
      {render_diff_recursive(diff)}
    " + + return mark_safe(result) From 174d30f7c1dbd56a33d2f4a5f401ca69163c302b Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 12:33:46 -0400 Subject: [PATCH 23/42] Avoid an error if the related_object doesn't have a class_path --- .../templates/nautobot_ssot/sync_header.html | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_header.html b/nautobot_ssot/templates/nautobot_ssot/sync_header.html index 69fc62a84..157553942 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_header.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_header.html @@ -11,16 +11,24 @@ {% if object.get_source_url %}
  • Data Sources
  • - - {{ object.job_result.related_object }} - + {% if object.job_result.related_object.class_path %} + + {{ object.job_result.related_object }} + + {% else %} + {{ object.source }} + {% endif %}
  • {% else %}
  • Data Targets
  • - - {{ object.job_result.related_object }} - + {% if object.job_result.related_object.class_path %} + + {{ object.job_result.related_object }} + + {% else %} + {{ object.target }} + {% endif %}
  • {% endif %}
  • {{ object }}
  • From 0a077e745d89d5f23deaba359a60e02014c3c897 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 13:37:37 -0400 Subject: [PATCH 24/42] Blacken --- nautobot_ssot/jobs/base.py | 2 +- nautobot_ssot/jobs/examples.py | 8 ++------ nautobot_ssot/migrations/0002_refinements.py | 18 +++++++++--------- .../migrations/0003_object_repr_blank.py | 8 ++++---- nautobot_ssot/tables.py | 13 ++++++++++++- nautobot_ssot/template_content.py | 1 + .../templatetags/dashboard_helpers.py | 5 +---- nautobot_ssot/views.py | 1 + tasks.py | 4 +++- 9 files changed, 34 insertions(+), 26 deletions(-) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index 1319af283..217b5f973 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -127,7 +127,7 @@ def _get_vars(cls): """ vars = super()._get_vars() if hasattr(cls, "dry_run"): - vars['dry_run'] = cls.dry_run + vars["dry_run"] = cls.dry_run return vars def as_form(self, data=None, files=None, initial=None): diff --git a/nautobot_ssot/jobs/examples.py b/nautobot_ssot/jobs/examples.py index f4919ada1..f57c8000c 100644 --- a/nautobot_ssot/jobs/examples.py +++ b/nautobot_ssot/jobs/examples.py @@ -19,9 +19,7 @@ class Meta: @classmethod def data_mappings(cls): - return ( - DataMapping("site slug", None, "Site", reverse("dcim:site_list")), - ) + return (DataMapping("site slug", None, "Site", reverse("dcim:site_list")),) def sync_data(self): """Perform data sync into Nautobot.""" @@ -50,9 +48,7 @@ class Meta: @classmethod def data_mappings(cls): - return ( - DataMapping("Site", reverse("dcim:site_list"), "site slug", None), - ) + return (DataMapping("Site", reverse("dcim:site_list"), "site slug", None),) def sync_data(self): """Perform data sync from Nautobot.""" diff --git a/nautobot_ssot/migrations/0002_refinements.py b/nautobot_ssot/migrations/0002_refinements.py index f6f61c9cc..ed59bf28e 100644 --- a/nautobot_ssot/migrations/0002_refinements.py +++ b/nautobot_ssot/migrations/0002_refinements.py @@ -6,23 +6,23 @@ class Migration(migrations.Migration): dependencies = [ - ('nautobot_ssot', '0001_initial'), + ("nautobot_ssot", "0001_initial"), ] operations = [ migrations.RenameField( - model_name='synclogentry', - old_name='changed_object_id', - new_name='synced_object_id', + model_name="synclogentry", + old_name="changed_object_id", + new_name="synced_object_id", ), migrations.RenameField( - model_name='synclogentry', - old_name='changed_object_type', - new_name='synced_object_type', + model_name="synclogentry", + old_name="changed_object_type", + new_name="synced_object_type", ), migrations.AlterField( - model_name='synclogentry', - name='diff', + model_name="synclogentry", + name="diff", field=models.JSONField(blank=True, null=True), ), ] diff --git a/nautobot_ssot/migrations/0003_object_repr_blank.py b/nautobot_ssot/migrations/0003_object_repr_blank.py index 7eda814eb..ece19707c 100644 --- a/nautobot_ssot/migrations/0003_object_repr_blank.py +++ b/nautobot_ssot/migrations/0003_object_repr_blank.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('nautobot_ssot', '0002_refinements'), + ("nautobot_ssot", "0002_refinements"), ] operations = [ migrations.AlterField( - model_name='synclogentry', - name='object_repr', - field=models.CharField(blank=True, default='', editable=False, max_length=200), + model_name="synclogentry", + name="object_repr", + field=models.CharField(blank=True, default="", editable=False, max_length=200), ), ] diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 536321045..6d76ab69f 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -179,6 +179,17 @@ class SyncLogEntryTable(BaseTable): class Meta(BaseTable.Meta): model = SyncLogEntry - fields = ("pk", "timestamp", "sync", "action", "synced_object_type", "synced_object", "object_change", "status", "diff", "message") + fields = ( + "pk", + "timestamp", + "sync", + "action", + "synced_object_type", + "synced_object", + "object_change", + "status", + "diff", + "message", + ) default_columns = ("pk", "timestamp", "sync", "action", "synced_object", "status", "diff", "message") order_by = ("-timestamp",) diff --git a/nautobot_ssot/template_content.py b/nautobot_ssot/template_content.py index 5ae023fb2..0595dc6c3 100644 --- a/nautobot_ssot/template_content.py +++ b/nautobot_ssot/template_content.py @@ -23,4 +23,5 @@ def buttons(self): except Sync.DoesNotExist: return "" + template_extensions = [JobResultSyncLink] diff --git a/nautobot_ssot/templatetags/dashboard_helpers.py b/nautobot_ssot/templatetags/dashboard_helpers.py index e44e60295..e8428da13 100644 --- a/nautobot_ssot/templatetags/dashboard_helpers.py +++ b/nautobot_ssot/templatetags/dashboard_helpers.py @@ -15,7 +15,4 @@ def dashboard_data(sync_worker_class, queryset, kind="source"): records = queryset.filter(source=sync_worker_class.name).order_by("-start_time") else: records = queryset.filter(target=sync_worker_class.name).order_by("-start_time") - return { - "statuses": [record.job_result.status for record in records[:10]], - "count": records.count() - } + return {"statuses": [record.job_result.status for record in records[:10]], "count": records.count()} diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 564b732f1..b8cab48a6 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -75,6 +75,7 @@ def get(self, request, class_path): }, ) + class SyncListView(ObjectListView): """View for listing Sync records.""" diff --git a/tasks.py b/tasks.py index f41838cfb..1c09e0c33 100644 --- a/tasks.py +++ b/tasks.py @@ -259,7 +259,9 @@ def sql_import(context): docker_compose(context, "up -d postgres") time.sleep(2) context.run(f"docker cp nautobot_backup.dump nautobot-ssot_postgres_1:/tmp/") - docker_compose(context, 'exec postgres sh -c "psql -h localhost -d nautobot -U nautbot < /tmp/nautobot_backup.dump"', pty=True) + docker_compose( + context, 'exec postgres sh -c "psql -h localhost -d nautobot -U nautbot < /tmp/nautobot_backup.dump"', pty=True + ) # ------------------------------------------------------------------------------ From 22dfb438c2a7e4b4e145b0ce7acc36d1cc1a7d5d Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 14:39:52 -0400 Subject: [PATCH 25/42] Linting --- nautobot_ssot/filters.py | 7 +++- nautobot_ssot/forms.py | 4 +++ nautobot_ssot/jobs/__init__.py | 2 ++ nautobot_ssot/jobs/base.py | 22 ++++++++---- nautobot_ssot/jobs/examples.py | 8 +++++ nautobot_ssot/models.py | 6 ++++ nautobot_ssot/navigation.py | 3 +- nautobot_ssot/tables.py | 8 +++++ nautobot_ssot/template_content.py | 5 +++ .../templatetags/dashboard_helpers.py | 3 ++ nautobot_ssot/templatetags/render_diff.py | 35 +++++++++++++++---- .../templatetags/shorter_timedelta.py | 2 ++ nautobot_ssot/views.py | 10 +++++- pyproject.toml | 2 ++ tasks.py | 2 +- 15 files changed, 101 insertions(+), 18 deletions(-) diff --git a/nautobot_ssot/filters.py b/nautobot_ssot/filters.py index eff2937a0..8fbc1da78 100644 --- a/nautobot_ssot/filters.py +++ b/nautobot_ssot/filters.py @@ -12,6 +12,8 @@ class SyncFilter(BaseFilterSet): """Filter capabilities for SyncOverview instances.""" class Meta: + """Metaclass attributes of SyncFilter.""" + model = Sync fields = ["dry_run", "job_result"] @@ -22,10 +24,13 @@ class SyncLogEntryFilter(BaseFilterSet): q = django_filters.CharFilter(method="search", label="Search") class Meta: + """Metaclass attributes of SyncLogEntryFilter.""" + model = SyncLogEntry fields = ["sync", "action", "status", "synced_object_type"] - def search(self, queryset, name, value): + def search(self, queryset, _name, value): # pylint: disable=no-self-use + """String search of SyncLogEntry records.""" if not value.strip(): return queryset return queryset.filter(Q(diff__icontains=value) | Q(message_icontains=value)) diff --git a/nautobot_ssot/forms.py b/nautobot_ssot/forms.py index 0ccb384a9..320ca2570 100644 --- a/nautobot_ssot/forms.py +++ b/nautobot_ssot/forms.py @@ -14,6 +14,8 @@ class SyncFilterForm(BootstrapMixin, forms.ModelForm): dry_run = forms.ChoiceField(choices=BOOLEAN_WITH_BLANK_CHOICES, required=False) class Meta: + """Metaclass attributes of SyncFilterForm.""" + model = Sync fields = ["dry_run"] @@ -27,6 +29,8 @@ class SyncLogEntryFilterForm(BootstrapMixin, forms.ModelForm): status = forms.ChoiceField(choices=add_blank_choice(SyncLogEntryStatusChoices), required=False) class Meta: + """Metaclass attributes of SyncLogEntryFilterForm.""" + model = SyncLogEntry fields = ["sync", "action", "status"] diff --git a/nautobot_ssot/jobs/__init__.py b/nautobot_ssot/jobs/__init__.py index e85afe412..3c77cfa72 100644 --- a/nautobot_ssot/jobs/__init__.py +++ b/nautobot_ssot/jobs/__init__.py @@ -1,3 +1,5 @@ +"""Plugin provision of Nautobot Job subclasses.""" + from nautobot.extras.jobs import get_jobs from .base import DataSource, DataTarget diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index 217b5f973..30cc2279b 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -6,6 +6,9 @@ from django.utils import timezone from django.utils.functional import classproperty +# pylint-django doesn't understand classproperty, and complains unnecessarily. We disable this specific warning: +# pylint: disable=no-self-argument + import structlog from nautobot.extras.jobs import BaseJob, BooleanVar @@ -41,9 +44,9 @@ def sync_data(self): - self.sync (Sync instance tracking this job execution) - self.job_result (as per Job API) """ - pass + pass # pylint: disable=unnecessary-pass - def lookup_object(self, model_name, unique_id): + def lookup_object(self, model_name, unique_id): # pylint: disable=no-self-use,unused-argument """Look up the Nautobot record and associated ObjectChange, if any, identified by the args. Optional helper method used to build more detailed/accurate SyncLogEntry records from DiffSync logs. @@ -75,7 +78,7 @@ def config_information(cls): """ return {} - def sync_log( + def sync_log( # pylint: disable=too-many-arguments self, action, status, @@ -121,14 +124,14 @@ def _structlog_to_sync_log_entry(self, _logger, _log_method, event_dict): @classmethod def _get_vars(cls): - """Extend Job._get_vars() to include `dry_run` variable. + """Extend Job._get_vars to include `dry_run` variable. Workaround for https://github.com/netbox-community/netbox/issues/5529 """ - vars = super()._get_vars() + got_vars = super()._get_vars() if hasattr(cls, "dry_run"): - vars["dry_run"] = cls.dry_run - return vars + got_vars["dry_run"] = cls.dry_run + return got_vars def as_form(self, data=None, files=None, initial=None): """Render this instance as a Django form for user inputs, including a "Dry run" field.""" @@ -161,6 +164,7 @@ def data_target_icon(cls): def run(self, data, commit): """Job entry point from Nautobot - do not override!""" + # pylint: disable=attribute-defined-outside-init self.sync = Sync.objects.create( source=self.data_source, target=self.data_target, @@ -192,10 +196,12 @@ class DataSource(DataSyncBaseJob): @classproperty def data_target(cls): + """For a DataSource this is always Nautobot.""" return "Nautobot" @classproperty def data_target_icon(cls): + """For a DataSource this is always the Nautobot logo.""" return static("img/nautobot_logo.png") @@ -206,8 +212,10 @@ class DataTarget(DataSyncBaseJob): @classproperty def data_source(cls): + """For a DataTarget this is always Nautobot.""" return "Nautobot" @classproperty def data_source_icon(cls): + """For a DataTarget this is always the Nautobot logo.""" return static("img/nautobot_logo.png") diff --git a/nautobot_ssot/jobs/examples.py b/nautobot_ssot/jobs/examples.py index f57c8000c..b90f4ad62 100644 --- a/nautobot_ssot/jobs/examples.py +++ b/nautobot_ssot/jobs/examples.py @@ -1,3 +1,5 @@ +"""Sample data-source and data-target Jobs.""" + from django.urls import reverse from nautobot.dcim.models import Site @@ -13,12 +15,15 @@ class ExampleDataSource(DataSource, Job): site_slug = StringVar(description="Site to create or update", default="") class Meta: + """Metaclass attributes of ExampleDataSource.""" + name = "Example Data Source" description = "An example of a 'data source' Job for loading data into Nautobot from elsewhere." data_source = "Dummy Data" @classmethod def data_mappings(cls): + """This Job maps a site slug to a Site object.""" return (DataMapping("site slug", None, "Site", reverse("dcim:site_list")),) def sync_data(self): @@ -42,12 +47,15 @@ class ExampleDataTarget(DataTarget, Job): site_slug = StringVar(description="Site to sync to an imaginary data target", default="") class Meta: + """Metaclass attributes of ExampleDataTarget.""" + name = "Example Data Target" description = "An example of a 'data target' Job for loading data from Nautobot into elsewhere." data_target = "Dummy Data" @classmethod def data_mappings(cls): + """This Job maps a Site object to a site slug.""" return (DataMapping("Site", reverse("dcim:site_list"), "site slug", None),) def sync_data(self): diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index c3a130793..17b2d96f5 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -52,12 +52,16 @@ class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): job_result = models.ForeignKey(to=JobResult, on_delete=models.PROTECT, blank=True, null=True) class Meta: + """Metaclass attributes of Sync model.""" + ordering = ["start_time"] def __str__(self): + """String representation of a Sync instance.""" return f"{self.source} → {self.target}, {date_format(self.start_time, format=settings.SHORT_DATETIME_FORMAT)}" def get_absolute_url(self): + """Get the detail-view URL for this instance.""" return reverse("plugins:nautobot_ssot:sync", kwargs={"pk": self.pk}) @classmethod @@ -147,6 +151,8 @@ class SyncLogEntry(BaseModel): message = models.CharField(max_length=511, blank=True) class Meta: + """Metaclass attributes of SyncLogEntry.""" + verbose_name_plural = "sync log entries" ordering = ["sync", "timestamp"] diff --git a/nautobot_ssot/navigation.py b/nautobot_ssot/navigation.py index 290f215b8..193e16df7 100644 --- a/nautobot_ssot/navigation.py +++ b/nautobot_ssot/navigation.py @@ -1,7 +1,6 @@ """Plugin additions to the Nautobot navigation menu.""" -from nautobot.extras.plugins import PluginMenuItem, PluginMenuButton -from nautobot.utilities.choices import ButtonColorChoices +from nautobot.extras.plugins import PluginMenuItem menu_items = ( diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index 6d76ab69f..af69654bb 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -45,6 +45,8 @@ class DashboardTable(BaseTable): dry_run = TemplateColumn(template_code=DRY_RUN_LABEL, verbose_name="Type") class Meta(BaseTable.Meta): + """Metaclass attributes of DashboardTable.""" + model = Sync fields = ["source", "target", "start_time", "status", "dry_run"] order_by = ["-start_time"] @@ -99,6 +101,8 @@ class SyncTable(BaseTable): ) class Meta(BaseTable.Meta): + """Metaclass attributes of SyncTable.""" + model = Sync fields = ( "pk", @@ -137,6 +141,8 @@ class SyncTableSingleSourceOrTarget(SyncTable): """Subclass of SyncTable with fewer default columns.""" class Meta(SyncTable.Meta): + """Metaclass attributes of SyncTableSingleSourceOrTarget.""" + default_columns = ( "start_time", "status", @@ -178,6 +184,8 @@ class SyncLogEntryTable(BaseTable): object_change = LinkColumn() class Meta(BaseTable.Meta): + """Metaclass attributes of SyncLogEntryTable.""" + model = SyncLogEntry fields = ( "pk", diff --git a/nautobot_ssot/template_content.py b/nautobot_ssot/template_content.py index 0595dc6c3..e53ac772f 100644 --- a/nautobot_ssot/template_content.py +++ b/nautobot_ssot/template_content.py @@ -1,9 +1,13 @@ +"""Plugin template content extensions of base Nautobot views.""" + from django.urls import reverse from nautobot.extras.plugins import PluginTemplateExtension from nautobot_ssot.models import Sync +# pylint: disable=abstract-method + class JobResultSyncLink(PluginTemplateExtension): """Add button linking to Sync data for relevant JobResults.""" @@ -11,6 +15,7 @@ class JobResultSyncLink(PluginTemplateExtension): model = "extras.jobresult" def buttons(self): + """Inject a custom button into the JobResult detail view, if applicable.""" try: sync = Sync.objects.get(job_result=self.context["object"]) return f""" diff --git a/nautobot_ssot/templatetags/dashboard_helpers.py b/nautobot_ssot/templatetags/dashboard_helpers.py index e8428da13..5845e46b5 100644 --- a/nautobot_ssot/templatetags/dashboard_helpers.py +++ b/nautobot_ssot/templatetags/dashboard_helpers.py @@ -1,3 +1,5 @@ +"""Helper templatetag for use with the "dashboard" UI view.""" + import logging from django import template @@ -11,6 +13,7 @@ @register.inclusion_tag("nautobot_ssot/templatetags/dashboard_data.html") def dashboard_data(sync_worker_class, queryset, kind="source"): + """Render data about the sync history of a specific data-source or data-target.""" if kind == "source": records = queryset.filter(source=sync_worker_class.name).order_by("-start_time") else: diff --git a/nautobot_ssot/templatetags/render_diff.py b/nautobot_ssot/templatetags/render_diff.py index 1a60e0cef..2d3fadd9c 100644 --- a/nautobot_ssot/templatetags/render_diff.py +++ b/nautobot_ssot/templatetags/render_diff.py @@ -1,13 +1,37 @@ +"""Template tag for rendering a DiffSync diff dictionary in a more human-readable form.""" + from django import template -from django.utils.safestring import mark_safe +from django.utils.html import format_html register = template.Library() def render_diff_recursive(diff): + """Recursively render a DiffSync diff dictionary representation to nested list elements. + + Example: + { + location: + ams: + +: {name: "Amsterdam"} + -: {name: "amsterdam"} + device: + ams01: + +: {} + } + + would render as: + + * location + ! ams + + name: Amsterdam + - name: amsterdam + * device + + ams01 + """ result = "" - for type, children in diff.items(): + for record_type, children in diff.items(): child_result = "" for child, child_diffs in children.items(): if "+" in child_diffs and "-" not in child_diffs: @@ -29,9 +53,8 @@ def render_diff_recursive(diff): if child_diffs: child_result += render_diff_recursive(child_diffs) - child_result += f"" - child_result = f"
      {child_result}
    " - result += f"
  • {type}{child_result}
  • " + child_result += "" + result += f"
  • {record_type}
      {child_result}
  • " return result @@ -40,4 +63,4 @@ def render_diff(diff): """Render a DiffSync diff dict to HTML.""" result = f"
      {render_diff_recursive(diff)}
    " - return mark_safe(result) + return format_html(result) diff --git a/nautobot_ssot/templatetags/shorter_timedelta.py b/nautobot_ssot/templatetags/shorter_timedelta.py index cbf9f0455..2b0be7aa1 100644 --- a/nautobot_ssot/templatetags/shorter_timedelta.py +++ b/nautobot_ssot/templatetags/shorter_timedelta.py @@ -1,3 +1,5 @@ +"""Simple template tag to render a shorter representation of a timedelta object.""" + from django import template from django.utils.html import format_html diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index b8cab48a6..0c80bc61d 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -28,6 +28,7 @@ class DashboardView(ObjectListView): template_name = "nautobot_ssot/dashboard.html" def extra_context(self): + """Extend the view context with additional details.""" data_sources, data_targets = get_data_jobs() context = { "queryset": self.queryset, @@ -55,9 +56,11 @@ class DataSourceTargetView(ContentTypePermissionRequiredMixin, View): """Detail view of a given Data Source or Data Target Job.""" def get_required_permission(self): + """Permissions required to access this view.""" return "extras.view_job" def get(self, request, class_path): + """HTTP GET request handler.""" job_class = get_job(class_path) if not job_class or not issubclass(job_class, (DataSource, DataTarget)): raise Http404 @@ -87,6 +90,7 @@ class SyncListView(ObjectListView): template_name = "nautobot_ssot/history.html" def extra_context(self): + """Extend the view context with additional information.""" data_sources, data_targets = get_data_jobs() return { "data_sources": data_sources, @@ -143,7 +147,8 @@ class SyncLogEntriesView(ObjectListView): action_buttons = [] template_name = "nautobot_ssot/sync_logentries.html" - def get(self, request, pk): + def get(self, request, pk): # pylint: disable=arguments-differ + """HTTP GET request handler.""" instance = get_object_or_404(Sync.objects.all(), pk=pk) self.queryset = SyncLogEntry.objects.filter(sync=instance) @@ -151,6 +156,9 @@ def get(self, request, pk): class SyncChangeLogView(ObjectChangeLogView): + """View for monitoring the changelog of a Sync object.""" + + # TODO: remove this view, sync should not be a changeloggedmodel base_template = "nautobot_ssot/sync_header.html" diff --git a/pyproject.toml b/pyproject.toml index d37c0bccf..b3d3089ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,9 +79,11 @@ no-docstring-rgx="^(_|test_|Meta$)" [tool.pylint.messages_control] # Line length is enforced by Black, so pylint doesn't need to check it. # Pylint and Black disagree about how to format multi-line arrays; Black wins. +# "too-few-public-methods" is just plain noise. disable = """, line-too-long, bad-continuation, + too-few-public-methods """ [tool.pylint.miscellaneous] diff --git a/tasks.py b/tasks.py index 1c09e0c33..d116d4d06 100644 --- a/tasks.py +++ b/tasks.py @@ -258,7 +258,7 @@ def sql_import(context): """Import nautobot_backup.dump into the database.""" docker_compose(context, "up -d postgres") time.sleep(2) - context.run(f"docker cp nautobot_backup.dump nautobot-ssot_postgres_1:/tmp/") + context.run("docker cp nautobot_backup.dump nautobot-ssot_postgres_1:/tmp/") docker_compose( context, 'exec postgres sh -c "psql -h localhost -d nautobot -U nautbot < /tmp/nautobot_backup.dump"', pty=True ) From 2f2521f83fbb10a60aba6f0bd73fe2c8c8c99e2a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 14:59:05 -0400 Subject: [PATCH 26/42] Regenerate migrations and remove ChangeLoggedModel from Sync --- nautobot_ssot/migrations/0001_initial.py | 35 ++++++++----------- nautobot_ssot/migrations/0002_refinements.py | 28 --------------- .../migrations/0003_object_repr_blank.py | 18 ---------- nautobot_ssot/models.py | 8 +++-- .../templates/nautobot_ssot/sync_header.html | 7 ---- nautobot_ssot/urls.py | 8 +---- nautobot_ssot/views.py | 8 ----- 7 files changed, 21 insertions(+), 91 deletions(-) delete mode 100644 nautobot_ssot/migrations/0002_refinements.py delete mode 100644 nautobot_ssot/migrations/0003_object_repr_blank.py diff --git a/nautobot_ssot/migrations/0001_initial.py b/nautobot_ssot/migrations/0001_initial.py index 955b3106a..debd9e03c 100644 --- a/nautobot_ssot/migrations/0001_initial.py +++ b/nautobot_ssot/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 3.1.11 on 2021-06-01 20:30 +# Generated by Django 3.1.12 on 2021-06-18 18:43 -import django.core.serializers.json from django.db import migrations, models import django.db.models.deletion import uuid @@ -11,8 +10,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("extras", "0005_configcontext_device_types"), ("contenttypes", "0002_remove_content_type_name"), + ("extras", "0005_configcontext_device_types"), ] operations = [ @@ -25,12 +24,6 @@ class Migration(migrations.Migration): default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True ), ), - ("created", models.DateField(auto_now_add=True, null=True)), - ("last_updated", models.DateTimeField(auto_now=True, null=True)), - ( - "_custom_field_data", - models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), - ), ("source", models.CharField(max_length=64)), ("target", models.CharField(max_length=64)), ("start_time", models.DateTimeField(null=True)), @@ -59,19 +52,10 @@ class Migration(migrations.Migration): ("timestamp", models.DateTimeField(auto_now_add=True)), ("action", models.CharField(max_length=32)), ("status", models.CharField(max_length=32)), - ("diff", models.JSONField()), - ("changed_object_id", models.UUIDField(blank=True, null=True)), - ("object_repr", models.CharField(editable=False, max_length=200)), + ("diff", models.JSONField(blank=True, null=True)), + ("synced_object_id", models.UUIDField(blank=True, null=True)), + ("object_repr", models.CharField(blank=True, default="", editable=False, max_length=200)), ("message", models.CharField(blank=True, max_length=511)), - ( - "changed_object_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="contenttypes.contenttype", - ), - ), ( "object_change", models.ForeignKey( @@ -87,6 +71,15 @@ class Migration(migrations.Migration): to="nautobot_ssot.sync", ), ), + ( + "synced_object_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="contenttypes.contenttype", + ), + ), ], options={ "verbose_name_plural": "sync log entries", diff --git a/nautobot_ssot/migrations/0002_refinements.py b/nautobot_ssot/migrations/0002_refinements.py deleted file mode 100644 index ed59bf28e..000000000 --- a/nautobot_ssot/migrations/0002_refinements.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.1.11 on 2021-06-09 14:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("nautobot_ssot", "0001_initial"), - ] - - operations = [ - migrations.RenameField( - model_name="synclogentry", - old_name="changed_object_id", - new_name="synced_object_id", - ), - migrations.RenameField( - model_name="synclogentry", - old_name="changed_object_type", - new_name="synced_object_type", - ), - migrations.AlterField( - model_name="synclogentry", - name="diff", - field=models.JSONField(blank=True, null=True), - ), - ] diff --git a/nautobot_ssot/migrations/0003_object_repr_blank.py b/nautobot_ssot/migrations/0003_object_repr_blank.py deleted file mode 100644 index ece19707c..000000000 --- a/nautobot_ssot/migrations/0003_object_repr_blank.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.12 on 2021-06-17 15:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("nautobot_ssot", "0002_refinements"), - ] - - operations = [ - migrations.AlterField( - model_name="synclogentry", - name="object_repr", - field=models.CharField(blank=True, default="", editable=False, max_length=200), - ), - ] diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index 17b2d96f5..ff4d276ca 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -27,12 +27,16 @@ from django.utils.timezone import now from nautobot.core.models import BaseModel -from nautobot.extras.models import ChangeLoggedModel, CustomFieldModel, JobResult, ObjectChange, RelationshipModel +from nautobot.extras.models import JobResult, ObjectChange +from nautobot.extras.utils import extras_features from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices -class Sync(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): +@extras_features( + "custom_links", +) +class Sync(BaseModel): """High-level overview of a data sync event/process/attempt. Essentially an extension of the JobResult model to add a few additional fields. diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_header.html b/nautobot_ssot/templates/nautobot_ssot/sync_header.html index 157553942..f224191ea 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_header.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_header.html @@ -56,12 +56,5 @@

    {% block title %}{{ object }}{% endblock %}

    - {% if perms.extras.view_objectchange %} - - {% endif %} {% endblock %} diff --git a/nautobot_ssot/urls.py b/nautobot_ssot/urls.py index 863332483..a54f0a6b9 100644 --- a/nautobot_ssot/urls.py +++ b/nautobot_ssot/urls.py @@ -2,7 +2,7 @@ from django.urls import path -from . import models, views +from . import views urlpatterns = [ path("", views.DashboardView.as_view(), name="dashboard"), @@ -11,12 +11,6 @@ path("history/", views.SyncListView.as_view(), name="sync_list"), path("history/delete/", views.SyncBulkDeleteView.as_view(), name="sync_bulk_delete"), path("history//", views.SyncView.as_view(), name="sync"), - path( - "history//changelog/", - views.SyncChangeLogView.as_view(), - name="sync_changelog", - kwargs={"model": models.Sync}, - ), path("history//delete/", views.SyncDeleteView.as_view(), name="sync_delete"), path("history//jobresult/", views.SyncJobResultView.as_view(), name="sync_jobresult"), path("history//logs/", views.SyncLogEntriesView.as_view(), name="sync_logentries"), diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 0c80bc61d..bfe46f927 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -8,7 +8,6 @@ from django.views.generic import View from nautobot.extras.jobs import get_job -from nautobot.extras.views import ObjectChangeLogView from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectListView, ObjectView from nautobot.utilities.views import ContentTypePermissionRequiredMixin @@ -155,13 +154,6 @@ def get(self, request, pk): # pylint: disable=arguments-differ return super().get(request) -class SyncChangeLogView(ObjectChangeLogView): - """View for monitoring the changelog of a Sync object.""" - - # TODO: remove this view, sync should not be a changeloggedmodel - base_template = "nautobot_ssot/sync_header.html" - - class SyncLogEntryListView(ObjectListView): """View for listing SyncLogEntry records.""" From 8aba02bc847e9e6756ae33185c48ebd6a015cdb8 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 15:02:11 -0400 Subject: [PATCH 27/42] Fix CI? --- development/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/Dockerfile b/development/Dockerfile index 40bab004c..ab4d12ce3 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -12,7 +12,7 @@ COPY poetry.lock pyproject.toml /source/ RUN poetry install --no-interaction --no-ansi --no-root # Add worker plugin(s) if present -COPY packages/*.whl /tmp/packages/ +COPY packages/placeholder packages/*.whl /tmp/packages/ RUN pip install /tmp/packages/*.whl # Copy in the rest of the source code and install local Nautobot plugin From 3e2f836742ad3da38efa040b46aeb650d3b19e80 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 15:20:21 -0400 Subject: [PATCH 28/42] Fix sync_logentries view --- .../templates/nautobot_ssot/sync_logentries.html | 1 + nautobot_ssot/views.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html b/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html index 7d97d6b19..97c6b88cb 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_logentries.html @@ -6,5 +6,6 @@
    {% include "responsive_table.html" %}
    + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
    {% endblock %} diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index bfe46f927..048533595 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -148,11 +148,15 @@ class SyncLogEntriesView(ObjectListView): def get(self, request, pk): # pylint: disable=arguments-differ """HTTP GET request handler.""" - instance = get_object_or_404(Sync.objects.all(), pk=pk) - self.queryset = SyncLogEntry.objects.filter(sync=instance) + self.instance = get_object_or_404(Sync.objects.all(), pk=pk) # pylint: disable=attribute-defined-outside-init + self.queryset = SyncLogEntry.objects.filter(sync=self.instance) return super().get(request) + def extra_context(self): + """Add additional context to the view.""" + return {"active_tab": "logentries", "object": self.instance} + class SyncLogEntryListView(ObjectListView): """View for listing SyncLogEntry records.""" From c70c89b46e5adfbb541bc8a1d3563f8971ae6e66 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 15:25:58 -0400 Subject: [PATCH 29/42] Another CI fix? --- development/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/Dockerfile b/development/Dockerfile index ab4d12ce3..035d248a9 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -13,7 +13,7 @@ RUN poetry install --no-interaction --no-ansi --no-root # Add worker plugin(s) if present COPY packages/placeholder packages/*.whl /tmp/packages/ -RUN pip install /tmp/packages/*.whl +RUN for wheel in /tmp/packages/*.whl; do pip install $wheel; done # Copy in the rest of the source code and install local Nautobot plugin WORKDIR /source From d21477d2fe9e090c8e635835d42308dfea5b5777 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 18 Jun 2021 15:51:44 -0400 Subject: [PATCH 30/42] Another attempt at fixing CI --- development/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/Dockerfile b/development/Dockerfile index 035d248a9..85f6ca273 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -13,7 +13,7 @@ RUN poetry install --no-interaction --no-ansi --no-root # Add worker plugin(s) if present COPY packages/placeholder packages/*.whl /tmp/packages/ -RUN for wheel in /tmp/packages/*.whl; do pip install $wheel; done +RUN for wheel in /tmp/packages/*.whl; do [ -f "$wheel" ] || continue; pip install "$wheel"; done # Copy in the rest of the source code and install local Nautobot plugin WORKDIR /source From 0a5b269eeff0493df15d089888cdfbc9461eda78 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 23 Jun 2021 17:17:15 -0400 Subject: [PATCH 31/42] Various UI refinements --- nautobot_ssot/filters.py | 2 +- nautobot_ssot/models.py | 4 ++-- .../templates/nautobot_ssot/sync_detail.html | 23 ++++++++++++------- .../templates/nautobot_ssot/sync_header.html | 1 - .../templatetags/dashboard_data.html | 22 ++++++++++-------- .../templatetags/dashboard_helpers.py | 2 +- nautobot_ssot/views.py | 13 +++++++++++ 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/nautobot_ssot/filters.py b/nautobot_ssot/filters.py index 8fbc1da78..9409cb84a 100644 --- a/nautobot_ssot/filters.py +++ b/nautobot_ssot/filters.py @@ -33,4 +33,4 @@ def search(self, queryset, _name, value): # pylint: disable=no-self-use """String search of SyncLogEntry records.""" if not value.strip(): return queryset - return queryset.filter(Q(diff__icontains=value) | Q(message_icontains=value)) + return queryset.filter(Q(diff__icontains=value) | Q(message__icontains=value)) diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index ff4d276ca..51698d815 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -104,7 +104,7 @@ def get_source_url(self): if self.source == "Nautobot": return None return reverse( - "extras:job", + "plugins:nautobot_ssot:data_source", kwargs={"class_path": self.job_result.name}, ) @@ -113,7 +113,7 @@ def get_target_url(self): if self.target == "Nautobot": return None return reverse( - "extras:job", + "plugins:nautobot_ssot:data_target", kwargs={"class_path": self.job_result.name}, ) diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html index b64941aed..75204a9b9 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_detail.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_detail.html @@ -62,7 +62,7 @@ Status - {{ object.job_result.get_status_display }} + {% include 'extras/inc/job_label.html' with result=object.job_result %} Job result @@ -81,16 +81,20 @@ Actions Taken - + {{ object.num_created }} - + {{ object.num_updated }} - + {{ object.num_deleted }} - + {{ object.num_unchanged }} @@ -98,13 +102,16 @@ Outcomes - + {{ object.num_succeeded }} - + {{ object.num_failed }} - + {{ object.num_errored }} diff --git a/nautobot_ssot/templates/nautobot_ssot/sync_header.html b/nautobot_ssot/templates/nautobot_ssot/sync_header.html index f224191ea..4fdb99e0d 100644 --- a/nautobot_ssot/templates/nautobot_ssot/sync_header.html +++ b/nautobot_ssot/templates/nautobot_ssot/sync_header.html @@ -42,7 +42,6 @@ {% endif %}

    {% block title %}{{ object }}{% endblock %}

    - {% include 'inc/created_updated.html' %}
    {% custom_links object %}
    diff --git a/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html b/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html index 77d5c01ce..7fef93f6f 100644 --- a/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html +++ b/nautobot_ssot/templates/nautobot_ssot/templatetags/dashboard_data.html @@ -1,11 +1,13 @@ -{% for status in statuses %} -   +{% for sync in syncs %} + + +   {% endfor %} -{{ count }} +{{ count }} diff --git a/nautobot_ssot/templatetags/dashboard_helpers.py b/nautobot_ssot/templatetags/dashboard_helpers.py index 5845e46b5..392630714 100644 --- a/nautobot_ssot/templatetags/dashboard_helpers.py +++ b/nautobot_ssot/templatetags/dashboard_helpers.py @@ -18,4 +18,4 @@ def dashboard_data(sync_worker_class, queryset, kind="source"): records = queryset.filter(source=sync_worker_class.name).order_by("-start_time") else: records = queryset.filter(target=sync_worker_class.name).order_by("-start_time") - return {"statuses": [record.job_result.status for record in records[:10]], "count": records.count()} + return {"syncs": records[:10], "count": records.count()} diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index 048533595..90587c52b 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -7,8 +7,11 @@ from django.shortcuts import get_object_or_404, render from django.views.generic import View +from django_tables2 import RequestConfig + from nautobot.extras.jobs import get_job from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectListView, ObjectView +from nautobot.utilities.paginator import EnhancedPaginator from nautobot.utilities.views import ContentTypePermissionRequiredMixin from .filters import SyncFilter, SyncLogEntryFilter @@ -29,12 +32,22 @@ class DashboardView(ObjectListView): def extra_context(self): """Extend the view context with additional details.""" data_sources, data_targets = get_data_jobs() + # Override default table context to limit the maximum number of records shown + table = self.table(self.queryset, user=self.request.user) + RequestConfig( + self.request, + { + "paginator_class": EnhancedPaginator, + "per_page": 10, + }, + ).configure(table) context = { "queryset": self.queryset, "data_sources": data_sources, "data_targets": data_targets, "source": {}, "target": {}, + "table": table, } sync_ct = ContentType.objects.get_for_model(Sync) for source in context["data_sources"]: From 97eb87162d16e754e9d3cc3bbca2e81f4d0c5abe Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 24 Jun 2021 15:46:46 -0400 Subject: [PATCH 32/42] Update nautobot dependency, add some docs --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .travis.yml | 2 +- README.md | 20 ++---- docs/index.md | 83 ++++++++++++++++++++++- nautobot_ssot/__init__.py | 2 +- poetry.lock | 78 ++++++++++----------- pyproject.toml | 2 +- tasks.py | 2 +- 9 files changed, 128 insertions(+), 65 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7d78afc22..87b0a96cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,7 +5,7 @@ about: Report a reproducible bug in the current release of nautobot-ssot ### Environment * Python version: -* Nautobot version: +* Nautobot version: * nautobot-ssot version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index df23df317..cedb1320a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,7 +5,7 @@ about: Propose a new feature or enhancement --- ### Environment -* Nautobot version: +* Nautobot version: * nautobot-ssot version: n SyncLogEntry 1-->1 ObjectChange +JobResult 1<->1 Sync 1-->n SyncLogEntry """ from datetime import timedelta @@ -27,7 +27,7 @@ from django.utils.timezone import now from nautobot.core.models import BaseModel -from nautobot.extras.models import JobResult, ObjectChange +from nautobot.extras.models import JobResult from nautobot.extras.utils import extras_features from .choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices @@ -150,7 +150,6 @@ class SyncLogEntry(BaseModel): synced_object = GenericForeignKey(ct_field="synced_object_type", fk_field="synced_object_id") object_repr = models.CharField(max_length=200, blank=True, default="", editable=False) - object_change = models.ForeignKey(to=ObjectChange, on_delete=models.SET_NULL, blank=True, null=True) message = models.CharField(max_length=511, blank=True) diff --git a/nautobot_ssot/tables.py b/nautobot_ssot/tables.py index af69654bb..78058e964 100644 --- a/nautobot_ssot/tables.py +++ b/nautobot_ssot/tables.py @@ -181,7 +181,6 @@ class SyncLogEntryTable(BaseTable): diff = JSONColumn(orderable=False) message = TemplateColumn(template_code=MESSAGE_SPAN, orderable=False) synced_object = TemplateColumn(template_code=SYNCED_OBJECT) - object_change = LinkColumn() class Meta(BaseTable.Meta): """Metaclass attributes of SyncLogEntryTable.""" @@ -194,7 +193,6 @@ class Meta(BaseTable.Meta): "action", "synced_object_type", "synced_object", - "object_change", "status", "diff", "message", From ec17c2578758fdb724c9386aa5e61cb2f8c75c45 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 28 Jun 2021 16:19:48 -0400 Subject: [PATCH 40/42] Add some unit test coverage --- nautobot_ssot/jobs/base.py | 8 +- nautobot_ssot/migrations/0001_initial.py | 6 +- nautobot_ssot/models.py | 10 +- nautobot_ssot/tests/test_jobs.py | 101 ++++++++++++++++ nautobot_ssot/tests/test_models.py | 84 ++++++++++++++ nautobot_ssot/tests/test_views.py | 141 +++++++++++++++++++++++ nautobot_ssot/views.py | 2 + 7 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 nautobot_ssot/tests/test_jobs.py create mode 100644 nautobot_ssot/tests/test_models.py create mode 100644 nautobot_ssot/tests/test_views.py diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index 5b49633b0..548295d31 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -140,6 +140,13 @@ def _get_vars(cls): got_vars["dry_run"] = cls.dry_run return got_vars + def __init__(self): + """Initialize a Job.""" + super().__init__() + self.sync = None + self.kwargs = {} + self.commit = False + def as_form(self, data=None, files=None, initial=None): """Render this instance as a Django form for user inputs, including a "Dry run" field.""" form = super().as_form(data=data, files=files, initial=initial) @@ -171,7 +178,6 @@ def data_target_icon(cls): def run(self, data, commit): """Job entry point from Nautobot - do not override!""" - # pylint: disable=attribute-defined-outside-init self.sync = Sync.objects.create( source=self.data_source, target=self.data_target, diff --git a/nautobot_ssot/migrations/0001_initial.py b/nautobot_ssot/migrations/0001_initial.py index 58cb478b1..4af5f8c65 100644 --- a/nautobot_ssot/migrations/0001_initial.py +++ b/nautobot_ssot/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.12 on 2021-06-28 17:06 +# Generated by Django 3.1.12 on 2021-06-28 20:03 from django.db import migrations, models import django.db.models.deletion @@ -26,9 +26,9 @@ class Migration(migrations.Migration): ), ("source", models.CharField(max_length=64)), ("target", models.CharField(max_length=64)), - ("start_time", models.DateTimeField(null=True)), + ("start_time", models.DateTimeField(blank=True, null=True)), ("dry_run", models.BooleanField(default=False)), - ("diff", models.JSONField()), + ("diff", models.JSONField(blank=True)), ( "job_result", models.ForeignKey( diff --git a/nautobot_ssot/models.py b/nautobot_ssot/models.py index 03cb365a0..078704aad 100644 --- a/nautobot_ssot/models.py +++ b/nautobot_ssot/models.py @@ -45,13 +45,13 @@ class Sync(BaseModel): source = models.CharField(max_length=64, help_text="System data is read from") target = models.CharField(max_length=64, help_text="System data is written to") - start_time = models.DateTimeField(null=True) + start_time = models.DateTimeField(blank=True, null=True) # end_time is represented by the job_result.completed field dry_run = models.BooleanField( default=False, help_text="Report what data would be synced but do not make any changes" ) - diff = models.JSONField() + diff = models.JSONField(blank=True) job_result = models.ForeignKey(to=JobResult, on_delete=models.PROTECT, blank=True, null=True) @@ -95,13 +95,13 @@ def duration(self): """Total execution time of this Sync.""" if not self.start_time: return timedelta() # zero - if not self.job_result.completed: + if not self.job_result or not self.job_result.completed: return now() - self.start_time return self.job_result.completed - self.start_time def get_source_url(self): """Get the absolute url of the source worker associated with this instance.""" - if self.source == "Nautobot": + if self.source == "Nautobot" or not self.job_result: return None return reverse( "plugins:nautobot_ssot:data_source", @@ -110,7 +110,7 @@ def get_source_url(self): def get_target_url(self): """Get the absolute url of the target worker associated with this instance.""" - if self.target == "Nautobot": + if self.target == "Nautobot" or not self.job_result: return None return reverse( "plugins:nautobot_ssot:data_target", diff --git a/nautobot_ssot/tests/test_jobs.py b/nautobot_ssot/tests/test_jobs.py new file mode 100644 index 000000000..2fb9753a8 --- /dev/null +++ b/nautobot_ssot/tests/test_jobs.py @@ -0,0 +1,101 @@ +"""Test the Job classes in nautobot_ssot.""" + +from django.forms import HiddenInput +from django.test import TestCase + +from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from nautobot_ssot.jobs.base import DataSyncBaseJob +from nautobot_ssot.jobs import DataSource, DataTarget +from nautobot_ssot.models import SyncLogEntry + + +class BaseJobTestCase(TestCase): + """Test the DataSyncBaseJob class.""" + + job_class = DataSyncBaseJob + + def setUp(self): + """Per-test setup.""" + self.job = self.job_class() + + def test_sync_log(self): + """Test the sync_log() method.""" + self.job.run(data={"dry_run": True}, commit=True) + self.assertIsNotNone(self.job.sync) + # Minimal parameters + self.job.sync_log( + action=SyncLogEntryActionChoices.ACTION_CREATE, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + ) + # Maximal parameters + self.job.sync_log( + action=SyncLogEntryActionChoices.ACTION_DELETE, + status=SyncLogEntryStatusChoices.STATUS_ERROR, + message="Whoops!", + diff={"-": {"everything": "goodbye"}}, + synced_object=None, + object_repr="Nothing to delete", + ) + + self.assertEqual(2, SyncLogEntry.objects.count()) + + def test_as_form(self): + """Test the as_form() method.""" + form = self.job.as_form() + # Dry run flag defaults to true unless configured otherwise + self.assertTrue(form.fields["dry_run"].initial) + # Commit field is hidden to reduce user confusion + self.assertIsInstance(form.fields["_commit"].widget, HiddenInput) + + def test_data_source(self): + """Test the data_source property.""" + self.assertEqual(self.job.data_source, self.job_class.__name__) + + def test_data_target(self): + """Test the data_target property.""" + self.assertEqual(self.job.data_target, self.job_class.__name__) + + def test_data_source_icon(self): + """Test the data_source_icon property.""" + self.assertIsNone(self.job.data_source_icon) + + def test_data_target_icon(self): + """Test the data_target_icon property.""" + self.assertIsNone(self.job.data_target_icon) + + def test_run(self): + """Test the run() method.""" + self.job.run(data={"dry_run": True}, commit=True) + self.assertIsNotNone(self.job.sync) + self.assertEqual(self.job.sync.source, self.job.data_source) + self.assertEqual(self.job.sync.target, self.job.data_target) + self.assertTrue(self.job.sync.dry_run) + self.assertEqual(self.job.job_result, self.job.sync.job_result) # both are None + + +class DataSourceTestCase(BaseJobTestCase): + """Test the DataSource class.""" + + job_class = DataSource + + def test_data_target(self): + """Test the override of the data_target property.""" + self.assertEqual(self.job.data_target, "Nautobot") + + def test_data_target_icon(self): + """Test the override of the data_target_icon property.""" + self.assertEqual(self.job.data_target_icon, "/static/img/nautobot_logo.png") + + +class DataTargetTestCase(BaseJobTestCase): + """Test the DataTarget class.""" + + job_class = DataTarget + + def test_data_source(self): + """Test the override of the data_source property.""" + self.assertEqual(self.job.data_source, "Nautobot") + + def test_data_source_icon(self): + """Test the override of the data_source_icon property.""" + self.assertEqual(self.job.data_source_icon, "/static/img/nautobot_logo.png") diff --git a/nautobot_ssot/tests/test_models.py b/nautobot_ssot/tests/test_models.py new file mode 100644 index 000000000..724f75f2d --- /dev/null +++ b/nautobot_ssot/tests/test_models.py @@ -0,0 +1,84 @@ +"""Model test cases for nautobot_ssot.""" + +from datetime import timedelta +import time +import uuid + +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.utils.timezone import now + +from nautobot.extras.choices import JobResultStatusChoices +from nautobot.extras.models import Job, JobResult + +from nautobot_ssot.models import Sync + + +class SyncTestCase(TestCase): + """Tests for the Sync model.""" + + def setUp(self): + """Per-test setup function.""" + self.source_sync = Sync( + source="Some other system", + target="Nautobot", + dry_run=False, + start_time=None, + diff={}, + ) + self.source_sync.validated_save() + self.target_sync = Sync( + source="Nautobot", + target="Another system", + dry_run=False, + start_time=None, + diff={}, + ) + self.target_sync.validated_save() + + def test_duration(self): + """Test the duration property.""" + # Hasn't started yet, so no applicable duration + self.assertEqual(self.source_sync.duration, timedelta()) + self.source_sync.start_time = now() + time.sleep(1) + self.assertGreater(self.source_sync.duration, timedelta()) + self.source_sync.job_result = JobResult( + name="/plugins/nautobot_ssot.jobs.examples/ExampleDataSource", + obj_type=ContentType.objects.get_for_model(Job), + job_id=uuid.uuid4(), + ) + # Still running + time.sleep(1) + self.assertGreater(self.source_sync.duration, timedelta(seconds=1)) + # Completed + self.source_sync.job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED) + duration = self.source_sync.duration + time.sleep(1) + self.assertEqual(duration, self.source_sync.duration) + + def test_get_source_target_url(self): + """Test the get_source_url() and get_target_url() methods.""" + # No JobResult + self.assertIsNone(self.source_sync.get_source_url()) + self.assertIsNone(self.target_sync.get_target_url()) + # Source/target is Nautobot + self.assertIsNone(self.target_sync.get_source_url()) + self.assertIsNone(self.source_sync.get_target_url()) + + self.source_sync.job_result = JobResult( + name="/plugins/nautobot_ssot.jobs.examples/ExampleDataSource", + obj_type=ContentType.objects.get_for_model(Job), + job_id=uuid.uuid4(), + ) + self.target_sync.job_result = JobResult( + name="/plugins/nautobot_ssot.jobs.examples/ExampleDataTarget", + obj_type=ContentType.objects.get_for_model(Job), + job_id=uuid.uuid4(), + ) + + self.assertIsNotNone(self.source_sync.get_source_url()) + self.assertIsNotNone(self.target_sync.get_target_url()) + # Source/target is Nautobot, so still None + self.assertIsNone(self.target_sync.get_source_url()) + self.assertIsNone(self.source_sync.get_target_url()) diff --git a/nautobot_ssot/tests/test_views.py b/nautobot_ssot/tests/test_views.py new file mode 100644 index 000000000..7a659e86d --- /dev/null +++ b/nautobot_ssot/tests/test_views.py @@ -0,0 +1,141 @@ +"""View test cases for nautobot_ssot.""" + +from datetime import datetime +import uuid + +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse + +from nautobot.extras.models import Job, JobResult +from nautobot.users.models import ObjectPermission +from nautobot.utilities.testing import ViewTestCases +from nautobot.utilities.testing.utils import disable_warnings + +from nautobot_ssot.choices import SyncLogEntryActionChoices, SyncLogEntryStatusChoices +from nautobot_ssot.models import Sync, SyncLogEntry + + +class SyncViewsTestCase( # pylint: disable=too-many-ancestors + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + """Test various views associated with the Sync model.""" + + model = Sync + + @classmethod + def setUpTestData(cls): + """One-time setup of test data for this class.""" + for i in range(0, 3): + job_result = JobResult.objects.create( + name="plugins/nautobot_ssot.jobs.examples/ExampleDataSource", + obj_type=ContentType.objects.get_for_model(Job), + data={}, + job_id=uuid.uuid4(), + ) + Sync.objects.create( + source="Example Data Source", + target="Nautobot", + start_time=datetime.now(), + dry_run=bool(i % 2), + diff={}, + job_result=job_result, + ) + + def test_dashboard_without_permission(self): + """Test that the dashboard view enforces permissions correctly.""" + with disable_warnings("django.request"): + self.assertHttpStatus(self.client.get(reverse("plugins:nautobot_ssot:dashboard")), 403) + + def test_dashboard_with_permission(self): + """Test the dashboard view.""" + obj_perm = ObjectPermission(name="Test permission", actions=["view"]) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + + self.assertHttpStatus(self.client.get(reverse("plugins:nautobot_ssot:dashboard")), 200) + + def test_data_source_target_view_without_permission(self): + """Test that the DataSourceTargetView enforces permissions correctly.""" + with disable_warnings("django.request"): + self.assertHttpStatus( + self.client.get( + reverse( + "plugins:nautobot_ssot:data_source", + kwargs={"class_path": "plugins/nautobot_ssot.jobs.examples/ExampleDataSource"}, + ) + ), + 403, + ) + + # Just access to Sync isn't sufficient - also need view_job permissions + obj_perm = ObjectPermission(name="Test permission", actions=["view"]) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + + with disable_warnings("django.request"): + self.assertHttpStatus( + self.client.get( + reverse( + "plugins:nautobot_ssot:data_source", + kwargs={"class_path": "plugins/nautobot_ssot.jobs.examples/ExampleDataSource"}, + ) + ), + 403, + ) + + def test_data_source_target_view_with_permission(self): + """Test the DataSourceTargetView.""" + obj_perm = ObjectPermission(name="Test permission", actions=["view"]) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ContentType.objects.get_for_model(Job)) + self.assertHttpStatus( + self.client.get( + reverse( + "plugins:nautobot_ssot:data_source", + kwargs={"class_path": "plugins/nautobot_ssot.jobs.examples/ExampleDataSource"}, + ) + ), + 200, + ) + + +class SyncLogEntryViewsTestCase(ViewTestCases.ListObjectsViewTestCase): # pylint: disable=too-many-ancestors + """Test views related to the SyncLogEntry model.""" + + model = SyncLogEntry + + @classmethod + def setUpTestData(cls): + """One-time setup of test data for this class.""" + job_result = JobResult.objects.create( + name="plugins/nautobot_ssot.jobs.examples/ExampleDataSource", + obj_type=ContentType.objects.get_for_model(Job), + data={}, + job_id=uuid.uuid4(), + ) + sync = Sync.objects.create( + source="Example Data Source", + target="Nautobot", + start_time=datetime.now(), + dry_run=False, + diff={}, + job_result=job_result, + ) + + for i in range(0, 3): + SyncLogEntry.objects.create( + sync=sync, + action=SyncLogEntryActionChoices.ACTION_NO_CHANGE, + status=SyncLogEntryStatusChoices.STATUS_SUCCESS, + diff={"+": {"i": i}}, + synced_object=None, + object_repr="Placeholder", + message="Log message", + ) diff --git a/nautobot_ssot/views.py b/nautobot_ssot/views.py index d366607a0..97608d4ba 100644 --- a/nautobot_ssot/views.py +++ b/nautobot_ssot/views.py @@ -67,6 +67,8 @@ def extra_context(self): class DataSourceTargetView(ContentTypePermissionRequiredMixin, View): """Detail view of a given Data Source or Data Target Job.""" + additional_permissions = ("nautobot_ssot.view_sync",) + def get_required_permission(self): """Permissions required to access this view.""" return "extras.view_job" From c0f3ee8d2e9843970b4a6f5c0a45cde83d7ea4fc Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 30 Jun 2021 14:30:10 -0400 Subject: [PATCH 41/42] Address review comment --- nautobot_ssot/__init__.py | 4 +++- nautobot_ssot/jobs/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index cc7d1deeb..ee9e85b81 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -23,7 +23,9 @@ class NautobotSSOTPluginConfig(PluginConfig): required_settings = [] min_version = "1.0.3" max_version = "1.9999" - default_settings = {} + default_settings = { + "hide_example_jobs": False, + } caching_config = {} diff --git a/nautobot_ssot/jobs/__init__.py b/nautobot_ssot/jobs/__init__.py index d00067cad..95d9d2931 100644 --- a/nautobot_ssot/jobs/__init__.py +++ b/nautobot_ssot/jobs/__init__.py @@ -7,7 +7,7 @@ from .base import DataSource, DataTarget from .examples import ExampleDataSource, ExampleDataTarget -if settings.PLUGINS_CONFIG.get("nautobot_ssot", {}).get("hide_example_jobs", False): +if settings.PLUGINS_CONFIG["nautobot_ssot"]["hide_example_jobs"]: jobs = [] else: jobs = [ExampleDataSource, ExampleDataTarget] From 220b6e3330fc805a9439cf65c1f782fe23b243e4 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 2 Jul 2021 13:30:20 -0400 Subject: [PATCH 42/42] Update docs and add screenshots --- README.md | 129 +----------------- docs/contributing.md | 5 + docs/developing_jobs.md | 40 ++++++ docs/development.md | 109 +++++++++++++++ docs/images/dashboard_initial.png | Bin 0 -> 71646 bytes docs/images/data_source_detail.png | Bin 0 -> 80958 bytes docs/images/job_result.png | Bin 0 -> 151486 bytes docs/images/run_job.png | Bin 0 -> 67853 bytes docs/images/sync_detail.png | Bin 0 -> 132432 bytes docs/images/sync_logs.png | Bin 0 -> 200575 bytes docs/index.md | 54 ++++---- mkdocs.yml | 3 + .../nautobot_ssot/data_source_target.html | 32 +++-- 13 files changed, 209 insertions(+), 163 deletions(-) create mode 100644 docs/contributing.md create mode 100644 docs/developing_jobs.md create mode 100644 docs/development.md create mode 100644 docs/images/dashboard_initial.png create mode 100644 docs/images/data_source_detail.png create mode 100644 docs/images/job_result.png create mode 100644 docs/images/run_job.png create mode 100644 docs/images/sync_detail.png create mode 100644 docs/images/sync_logs.png diff --git a/README.md b/README.md index 30db1c191..fd24f944d 100644 --- a/README.md +++ b/README.md @@ -37,128 +37,7 @@ The plugin behavior can be controlled with the following list of settings: ## Usage -### API - -TODO - -## Contributing - -Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of Nautobot through TravisCI. - -The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI. - -The project is following Network to Code software development guideline and is leveraging: - -- Black, Pylint, Bandit and pydocstyle for Python linting and formatting. -- Django unit test to ensure the plugin is working properly. - -### Development Environment - -The development environment can be used in 2 ways. First, with a local poetry environment if you wish to develop outside of Docker. Second, inside of a docker container. - -#### Invoke tasks - -The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: - -* `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest) -* `project_name`: the default docker compose project name (default: nautobot-ssot) -* `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) -* `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) -* `compose_dir`: the full path to a directory containing the project compose files -* `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) - -Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT_SSOT_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. - -#### Local Poetry Development Environment - -1. Copy `development/creds.example.env` to `development/creds.env` (This file will be ignored by git and docker) -2. Uncomment the `POSTGRES_HOST`, `REDIS_HOST`, and `NAUTOBOT_ROOT` variables in `development/creds.env` -3. Create an invoke.yml with the following contents at the root of the repo: - -```shell ---- -nautobot_ssot: - local: true - compose_files: - - "docker-compose.requirements.yml" -``` - -3. Run the following commands: - -```shell -poetry shell -poetry install -export $(cat development/dev.env | xargs) -export $(cat development/creds.env | xargs) -``` - -4. You can now run nautobot-server commands as you would from the [Nautobot documentation](https://nautobot.readthedocs.io/en/latest/) for example to start the development server: - -```shell -nautobot-server runserver 0.0.0.0:8080 --insecure -``` - -Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). - -#### Docker Development Environment - -This project is managed by [Python Poetry](https://python-poetry.org/) and has a few requirements to setup your development environment: - -1. Install Poetry, see the [Poetry Documentation](https://python-poetry.org/docs/#installation) for your operating system. -2. Install Docker, see the [Docker documentation](https://docs.docker.com/get-docker/) for your operating system. -3. If you wish to use any data sources / data target libraries, add the appropriate `*.whl` files to the `packages/` directory - they will be automatically installed as part of any Docker build. (TODO change this eventually) - -Once you have Poetry and Docker installed you can run the following commands to install all other development dependencies in an isolated python virtual environment: - -```shell -poetry shell -poetry install -invoke build start -``` - -Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). - -### CLI Helper Commands - -The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. - -Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT_SSOT_PYTHON_VER` and `INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` - -#### Docker dev environment - -```no-highlight - build Build all docker images. - debug Start Nautobot and its dependencies in debug mode. - destroy Destroy all containers and volumes. - restart Restart Nautobot and its dependencies. - start Start Nautobot and its dependencies in detached mode. - stop Stop Nautobot and its dependencies. -``` - -#### Utility - -```no-highlight - cli Launch a bash shell inside the running Nautobot container. - create-user Create a new user in django (default: admin), will prompt for password. - makemigrations Run Make Migration in Django. - nbshell Launch a nbshell session. -``` - -#### Testing - -```no-highlight - bandit Run bandit to validate basic static code security analysis. - black Run black to check that Python files adhere to its style standards. - flake8 This will run flake8 for the specified name and Python version. - pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. - pylint Run pylint code analysis. - tests Run all tests for this plugin. - unittest Run Django unit tests for the plugin. -``` - -### Project Documentation - -Project documentation is generated by [mkdocs](https://www.mkdocs.org/) from the documentation located in the docs folder. You can configure [readthedocs.io](https://readthedocs.io/) to point at this folder in your repo. For development purposes a `docker-compose.docs.yml` is also included. A container hosting the docs will be started using the invoke commands on [http://localhost:8001](http://localhost:8001), as changes are saved the docs will be automatically reloaded. +Refer to the [documentation](./docs/index.md) for usage details. ## Questions @@ -167,4 +46,8 @@ Sign up [here](http://slack.networktocode.com/) ## Screenshots -TODO +![Dashboard screenshot](./docs/images/dashboard_initial.png) + +![Data Source detail view](./docs/images/data_source_detail.png) + +![Sync detail view](./docs/images/sync_detail.png) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..6e86b3115 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,5 @@ +# Contributing + +Pull requests are welcomed and automatically built and tested against multiple versions of Python and multiple versions of Nautobot through Travis CI. + +Refer to the [development](./development.md) documentation for information on setting up a local development environment. diff --git a/docs/developing_jobs.md b/docs/developing_jobs.md new file mode 100644 index 000000000..a97ebbeed --- /dev/null +++ b/docs/developing_jobs.md @@ -0,0 +1,40 @@ +# Developing Data Source and Data Target Jobs + +A goal of this plugin is to make it relatively quick and straightforward to develop and integrate your own system-specific Data Sources and Data Targets into Nautobot with a common UI and user experience. + +Familiarity with [DiffSync](https://diffsync.readthedocs.io/en/latest/) and with developing [Nautobot Jobs](https://nautobot.readthedocs.io/en/latest/additional-features/jobs/) is recommended. + +In brief, the following general steps can be followed: + +1. Define one or more `DiffSyncModel` data model class(es) representing the common data record(s) to be synchronized between the two systems. + + * For each model class, implement the `create`, `update`, and `delete` DiffSyncModel APIs for writing data to the Data Target system. + +2. Define a `DiffSync` adapter class for loading initial data from Nautobot and constructing instances of each `DiffSyncModel` class to represent that data. +3. Define a `DiffSync` adapter class for loading initial data from the Data Source or Data Target system and constructing instances of the `DiffSyncModel` classes to represent that data. +4. Develop a Job class, derived from either the `DataSource` or `DataTarget` classes provided by this plugin, and implement its `sync_data` API function to make use of the DiffSync adapters and data models defined previously. Typically this will look something like: + + def sync_data(self): + """Sync data from Data Source to Nautobot.""" + self.log_info(message="Loading current data from Data Source...") + diffsync1 = DataSourceDiffSync(job=self, sync=self.sync) + diffsync1.load() + + self.log_info(message="Loading current data from Nautobot...") + diffsync2 = NautobotDiffSync(job=self, sync=self.sync) + diffsync2.load() + + diffsync_flags = DiffSyncFlags.CONTINUE_ON_FAILURE + + self.log_info(message="Calculating diffs...") + diff = diffsync1.diff_to(diffsync_1, flags=diffsync_flags) + self.sync.diff = diff.dict() + self.sync.save() + + if not self.kwargs["dry_run"]: + self.log_info(message="Syncing from Data Source to Nautobot...") + diffsync1.sync_to(diffsync2, flags=diffsync_flags) + self.log_info(message="Sync complete") + +5. Optionally, on your Job class, also implement the `lookup_object`, `data_mappings`, and/or `config_information` APIs (to provide more information to the end user about the details of this Job), as well as the various metadata properties on your Job's `Meta` inner class. Refer to the example Jobs provided in this plugin for examples and further details. +6. Install your Job via any of the supported Nautobot methods (installation into the `JOBS_ROOT` directory, inclusion in a Git repository, or packaging as part of a plugin) and it should automatically become available! diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 000000000..09bd8d9a1 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,109 @@ +# Development + +The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within Travis CI. + +The project is following Network to Code software development guideline and is leveraging: + +- Black, Pylint, Bandit and pydocstyle for Python linting and formatting. +- Django unit test to ensure the plugin is working properly. + +The development environment can be used in 2 ways. First, with a local poetry environment if you wish to develop outside of Docker. Second, inside of a docker container. + +## Invoke tasks + +The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: + +* `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest) +* `project_name`: the default docker compose project name (default: nautobot-ssot) +* `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) +* `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) +* `compose_dir`: the full path to a directory containing the project compose files +* `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) + +Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is simply setting an environment variable `INVOKE_NAUTOBOT_SSOT_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a yaml file. There is an example `invoke.yml` in this directory which can be used as a starting point. + +## Local Poetry Development Environment + +1. Copy `development/creds.example.env` to `development/creds.env` (This file will be ignored by git and docker) +2. Uncomment the `POSTGRES_HOST`, `REDIS_HOST`, and `NAUTOBOT_ROOT` variables in `development/creds.env` +3. Create an invoke.yml with the following contents at the root of the repo: + + --- + nautobot_ssot: + local: true + compose_files: + - "docker-compose.requirements.yml" + +4. Run the following commands: + + poetry shell + poetry install + export $(cat development/dev.env | xargs) + export $(cat development/creds.env | xargs) + +5. You can now run nautobot-server commands as you would from the [Nautobot documentation](https://nautobot.readthedocs.io/en/latest/) for example to start the development server: + + nautobot-server runserver 0.0.0.0:8080 --insecure + +Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). + +## Docker Development Environment + +This project is managed by [Python Poetry](https://python-poetry.org/) and has a few requirements to setup your development environment: + +1. Install Poetry, see the [Poetry Documentation](https://python-poetry.org/docs/#installation) for your operating system. +2. Install Docker, see the [Docker documentation](https://docs.docker.com/get-docker/) for your operating system. +3. If you wish to use any data sources / data target libraries, add the appropriate `*.whl` files to the `packages/` directory - they will be automatically installed as part of any Docker build. (TODO change this eventually) + +Once you have Poetry and Docker installed you can run the following commands to install all other development dependencies in an isolated python virtual environment: + +```shell +poetry shell +poetry install +invoke build start +``` + +Nautobot server can now be accessed at [http://localhost:8080](http://localhost:8080). + +## CLI Helper Commands + +The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. + +Each command can be executed with `invoke `. Environment variables `INVOKE_NAUTOBOT_SSOT_PYTHON_VER` and `INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke --help` + +### Docker dev environment + +```no-highlight + build Build all docker images. + debug Start Nautobot and its dependencies in debug mode. + destroy Destroy all containers and volumes. + restart Restart Nautobot and its dependencies. + start Start Nautobot and its dependencies in detached mode. + stop Stop Nautobot and its dependencies. +``` + +### Utility + +```no-highlight + cli Launch a bash shell inside the running Nautobot container. + create-user Create a new user in django (default: admin), will prompt for password. + makemigrations Run Make Migration in Django. + nbshell Launch a nbshell session. +``` + +### Testing + +```no-highlight + bandit Run bandit to validate basic static code security analysis. + black Run black to check that Python files adhere to its style standards. + flake8 This will run flake8 for the specified name and Python version. + pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. + pylint Run pylint code analysis. + tests Run all tests for this plugin. + unittest Run Django unit tests for the plugin. +``` + +## Project Documentation + +Project documentation is generated by [mkdocs](https://www.mkdocs.org/) from the documentation located in the docs folder. You can configure [readthedocs.io](https://readthedocs.io/) to point at this folder in your repo. For development purposes a `docker-compose.docs.yml` is also included. A container hosting the docs will be started using the invoke commands on [http://localhost:8001](http://localhost:8001), as changes are saved the docs will be automatically reloaded. + diff --git a/docs/images/dashboard_initial.png b/docs/images/dashboard_initial.png new file mode 100644 index 0000000000000000000000000000000000000000..67db24e4262f0fb6c0329dd2ce0b8c4426bc7abf GIT binary patch literal 71646 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 literal 0 HcmV?d00001 diff --git a/docs/images/data_source_detail.png b/docs/images/data_source_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..234f219e9fbb018f8cd14db31b9ae9b0b4a3600b GIT binary patch literal 80958 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 IbxsLQ0G359o&W#< literal 0 HcmV?d00001 diff --git a/docs/images/job_result.png b/docs/images/job_result.png new file mode 100644 index 0000000000000000000000000000000000000000..f8409b1c5ee03fd434c1dd44c29de26754b0f217 GIT binary patch literal 151486 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

    +24`t1 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 Gr~d;%@xlNA literal 0 HcmV?d00001 diff --git a/docs/images/run_job.png b/docs/images/run_job.png new file mode 100644 index 0000000000000000000000000000000000000000..c77605e6b5b71863d159c29023906f142ae173bc GIT binary patch literal 67853 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 S#Ka}wbJWG{N6sPtpMC>S?=b2B literal 0 HcmV?d00001 diff --git a/docs/images/sync_detail.png b/docs/images/sync_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..33c899aa31db3fdde60658b35800689ce27fc8d1 GIT binary patch literal 132432 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

    WAn=3rCJ3P8xMOADq8{MhO*}6cA(T$a?v2UUL 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 literal 0 HcmV?d00001 diff --git a/docs/images/sync_logs.png b/docs/images/sync_logs.png new file mode 100644 index 0000000000000000000000000000000000000000..e91920b202981047ecaf145d517991126f936944 GIT binary patch literal 200575 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index a9c230874..0b6b96267 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,14 +2,24 @@ A plugin for [Nautobot](https://github.com/nautobot/nautobot). This plugin facilitates integration and data synchronization between various "source of truth" (SoT) systems, with Nautobot acting as a central clearinghouse for data - a Single Source of Truth, if you will. -## Description - The `nautobot-ssot` plugin builds atop the [DiffSync](https://diffsync.readthedocs.io/en/latest/) Python library and Nautobot's [Jobs](https://nautobot.readthedocs.io/en/latest/additional-features/jobs/) feature. This enables the rapid development and integration of Jobs that can be run within Nautobot to pull data from other systems ("Data Sources") into Nautobot and/or push data from Nautobot into other systems ("Data Targets") as desired. Key features include the following: * A dashboard UI lists all registered Data Sources and Data Targets and provides a summary of the synchronization history. * The outcome of executing of a data synchronization Job is automatically saved to Nautobot's database for later review. * Detailed logging output generated by DiffSync is automatically captured and saved to the database as well. +--- + +![Dashboard screenshot](./images/dashboard_initial.png) + +--- + +![Data Source detail view](./images/data_source_detail.png) + +--- + +![Sync detail view](./images/sync_detail.png) + ## Installation The plugin is available as a Python package in PyPI and can be installed with `pip`: @@ -27,21 +37,27 @@ Once installed, the plugin needs to be enabled in your `nautobot_config.py`: PLUGINS = ["nautobot_ssot"] ``` -TODO finding and installing data target and data source jobs +This plugin provides examples of Data Source and Data Target jobs that you can run to get a feel for the capabilities of the plugin, but to get the most out of this plugin you will want to find other existing Jobs and/or create your own Jobs; these Jobs can be installed like any other Nautobot Job, i.e. via a plugin, by inclusion in a Git repository, or by manual installation of individual Job source files to Nautobot's `JOBS_ROOT` directory. ## Configuration -TODO +By default this plugin provides an example Data Source Job and example Data Target Job. Once you have other, more useful Jobs installed, these example Jobs can be disabled and removed from the UI by configuring `"hide_example_jobs"` to `True` in your `nautobot_config.py`: + +```python +PLUGINS_CONFIG = { + "nautobot_ssot": { + "hide_example_jobs": True, + } +} +``` ## Usage ### Dashboard -By default this plugin provides an example Data Source Job and example Data Target Job. (These can be disabled and removed from the UI by ... TODO) - The plugin dashboard UI can be accessed from the **Plugins > Single Source of Truth > Dashboard** menu item in Nautobot. -TODO insert screenshot here +![Dashboard](./images/dashboard_initial.png) The left side of the dashboard lists all discovered Data Sources and Data Targets. In a fresh installation this will include the "Example Data Source" and "Example Data Target"; when you install additional data synchronization Jobs they will be automatically discovered and included in the dashboard as well. @@ -51,7 +67,7 @@ The right side of the dashboard lists the ten most recent data syncs executed (i From the dashboard UI, you can click on the name of any given Data Source or Data Target to access a detailed view of the integration between this system and Nautobot. -TODO screenshot +![Data Source detail view](./images/data_source_detail.png) This view lists the configuration (if any) of the Data Source or Data Target, provides a table describing the types of data being mapped between Nautobot and the other system, and, at the bottom of the page, lists the history of data synchronization involving this system. @@ -59,17 +75,19 @@ This view lists the configuration (if any) of the Data Source or Data Target, pr To synchronize data between Nautobot and a given Data Source or Data Target, select the **Sync** button for the desired integration from either the Dashboard view or the detailed view. This will bring up a form similar to that of executing any other Nautobot Job. -TODO screenshot +![Job submission form](./images/run_job.png) Enter any appropriate parameters here, including selecting whether to execute the synchronization as a "dry run" (identifying data to be synchronized, but not actually making any changes to the system) or as an actual database update, and select **Run Job**. You will be redirected to a standard Nautobot "Job Result" view, which will update as the Job is enqueued, begins execution, and eventually completes. When execution is complete, an **SSoT Sync Details** button will appear at the top right of the page; you can select this button for a more detailed view of the outcome. +![Job Result view](./images/job_result.png) + ### Viewing a data sync record The detailed view of a single data synchronization attempt between Nautobot and a Data Source/Target can be accessed from the Job Result view as described in the previous section, or by navigating to **Plugins > Single Source of Truth > History** and selecting the desired record from the table presented in that view. -TODO screenshot +![Sync detail view](./images/sync_detail.png) This view describes in detail everything that occurred during the data synchronization attempt. The primary **Data Sync** tab summarizes the overall outcome of the sync attempt, including a view of the diffs (if any) identified by DiffSync and a summary of the actions taken (create, update, delete) and their outcomes (success, failure, error). @@ -77,18 +95,4 @@ The **Job Logs** tab shows any general status messages generated by the data syn The **Sync Logs** tab shows the logs captured from DiffSync regarding the individual data records being synchronized, details of any contents or changes of these records, and other detailed information. Sync logs can also be accessed directly via the **Plugins > Single Source of Truth > Logs** menu item if desired. -## API - -TODO - -## Developing a Data Source or Data Target Job - -A goal of this plugin is to make it relatively quick and straightforward to develop and integrate your own system-specific Data Sources and Data Targets into Nautobot with a common UI and user experience. - -Familiarity with DiffSync and with developing Nautobot Jobs is recommended. - -In brief, the following general steps can be followed: - -1. Define DiffSync data model(s) representing the common data record(s) to be synchronized between the two systems. -2. Define two DiffSync adapter classes for populating the data model - one for loading data from Nautobot itself, and one for loading data from the Data Source or Data Target system. -3. Develop a Job class, derived from either the `DataSource` or `DataTarget` classes provided by this plugin, and implement its `sync_data` API function to make use of the DiffSync adapters and data models defined previously. +![Sync logs view](./images/sync_logs.png) diff --git a/mkdocs.yml b/mkdocs.yml index 1e0796ae9..e0ecd29de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,3 +23,6 @@ markdown_extensions: permalink: true nav: - Introduction: "index.md" + - Developing Data Jobs: "developing_jobs.md" + - Developing the plugin itself: "development.md" + - Contributing: "contributing.md" diff --git a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html index c4f266268..ffb988cd8 100644 --- a/nautobot_ssot/templates/nautobot_ssot/data_source_target.html +++ b/nautobot_ssot/templates/nautobot_ssot/data_source_target.html @@ -53,23 +53,25 @@

    {% block title %}SSoT - {{ job_class }}{% endblock %}

    {{ job_class.data_target }}

    -
    -
    Configuration
    - - - - - - {% for parameter, value in job_class.config_information.items %} + {% if job_class.config_information %} +
    +
    Configuration
    +
    ParameterValue
    - - + + - {% endfor %} -
    {{ parameter }} - {% if value %}{{ value }}{% else %}—{% endif %} - ParameterValue
    -
    + {% for parameter, value in job_class.config_information.items %} + + {{ parameter }} + + {% if value %}{{ value }}{% else %}—{% endif %} + + + {% endfor %} + + + {% endif %}