From d00c5acfe9a8ec2931a1c09937ce4cdc7e1f9d88 Mon Sep 17 00:00:00 2001 From: Marek Zbroch Date: Fri, 21 Aug 2020 15:46:34 +0200 Subject: [PATCH 1/2] expose onboarding details in device view --- .../migrations/0002_onboardingdevice.py | 22 +++++ netbox_onboarding/models.py | 49 +++++++++++ netbox_onboarding/template_content.py | 47 ++++++++++ .../device_onboarding_table.html | 31 +++++++ netbox_onboarding/tests/test_models.py | 88 +++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 netbox_onboarding/migrations/0002_onboardingdevice.py create mode 100644 netbox_onboarding/template_content.py create mode 100644 netbox_onboarding/templates/netbox_onboarding/device_onboarding_table.html create mode 100644 netbox_onboarding/tests/test_models.py diff --git a/netbox_onboarding/migrations/0002_onboardingdevice.py b/netbox_onboarding/migrations/0002_onboardingdevice.py new file mode 100644 index 0000000..45b8503 --- /dev/null +++ b/netbox_onboarding/migrations/0002_onboardingdevice.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.10 on 2020-08-21 11:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("netbox_onboarding", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="OnboardingDevice", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ("enabled", models.BooleanField(default=True)), + ("device", models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="dcim.Device")), + ], + ), + ] diff --git a/netbox_onboarding/models.py b/netbox_onboarding/models.py index ed48bf6..7627880 100644 --- a/netbox_onboarding/models.py +++ b/netbox_onboarding/models.py @@ -11,7 +11,10 @@ See the License for the specific language governing permissions and limitations under the License. """ +from django.db.models.signals import post_save +from django.dispatch import receiver from django.db import models +from dcim.models import Device from django.urls import reverse from .choices import OnboardingStatusChoices, OnboardingFailChoices from .release import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_29 @@ -64,3 +67,49 @@ def get_absolute_url(self): from utilities.querysets import RestrictedQuerySet # pylint: disable=no-name-in-module, import-outside-toplevel objects = RestrictedQuerySet.as_manager() + + +class OnboardingDevice(models.Model): + """The status of each Onboarded Device is tracked in the OnboardingDevice table.""" + + device = models.OneToOneField(to="dcim.Device", on_delete=models.CASCADE) + enabled = models.BooleanField(default=True, help_text="Whether (re)onboarding of this device is permitted") + + @property + def last_check_attempt_date(self): + """Date of last onboarding attempt for a device.""" + try: + return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").created_on + except ValueError: + return "unknown" + + @property + def last_check_successful_date(self): + """Date of last successful onboarding for a device.""" + try: + return ( + OnboardingTask.objects.filter( + created_device=self.device, status=OnboardingStatusChoices.STATUS_SUCCEEDED + ) + .latest("created_on") + .created_on + ) + except ValueError: + return "unknown" + + @property + def status(self): + """Last onboarding status.""" + try: + return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").status + except ValueError: + return "unknown" + + @property + def last_ot(self): + """Last onboarding task.""" + try: + return OnboardingTask.objects.filter(created_device=self.device).latest("created_on") + except ValueError: + return None + diff --git a/netbox_onboarding/template_content.py b/netbox_onboarding/template_content.py new file mode 100644 index 0000000..b9aa7b9 --- /dev/null +++ b/netbox_onboarding/template_content.py @@ -0,0 +1,47 @@ +"""Onboarding template content. + +(c) 2020 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 extras.plugins import PluginTemplateExtension +from .models import OnboardingDevice + + +class DeviceContent(PluginTemplateExtension): # pylint: disable=abstract-method + """Table to show onboarding details on Device objects.""" + + model = "dcim.device" + + def right_page(self): + """Show table on right side of view.""" + onboarding = OnboardingDevice.objects.filter(device=self.context["object"]).first() + + if not onboarding.enabled: + return None + + status = onboarding.status + last_check_attempt_date = onboarding.last_check_attempt_date + last_check_successful_date = onboarding.last_check_successful_date + last_ot = onboarding.last_ot + + return self.render( + "netbox_onboarding/device_onboarding_table.html", + extra_context={ + "status": status, + "last_check_attempt_date": last_check_attempt_date, + "last_check_successful_date": last_check_successful_date, + "last_ot": last_ot, + }, + ) + + +template_extensions = [DeviceContent] diff --git a/netbox_onboarding/templates/netbox_onboarding/device_onboarding_table.html b/netbox_onboarding/templates/netbox_onboarding/device_onboarding_table.html new file mode 100644 index 0000000..2a20488 --- /dev/null +++ b/netbox_onboarding/templates/netbox_onboarding/device_onboarding_table.html @@ -0,0 +1,31 @@ +{% block content %} +
+
+ Device Onboarding +
+ + + + + + + + + + + + + + + +
DateStatusDate of last successLatest Task
+ {{ last_check_attempt_date }} + + {{ status }} + + {{ last_check_successful_date }} + + {{ last_ot.pk }} +
+
+{% endblock %} diff --git a/netbox_onboarding/tests/test_models.py b/netbox_onboarding/tests/test_models.py new file mode 100644 index 0000000..26bf95c --- /dev/null +++ b/netbox_onboarding/tests/test_models.py @@ -0,0 +1,88 @@ +"""Unit tests for netbox_onboarding OnboardingDevice model. + +(c) 2020 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 django.test import TestCase + +from dcim.models import Site, DeviceRole, DeviceType, Manufacturer, Device + +from netbox_onboarding.models import OnboardingTask +from netbox_onboarding.models import OnboardingDevice +from netbox_onboarding.choices import OnboardingStatusChoices + + +class OnboardingDeviceModelTestCase(TestCase): + """Test the Onboarding models.""" + + def setUp(self): + """Setup objects for Onboarding Model tests.""" + self.site = Site.objects.create(name="USWEST", slug="uswest") + manufacturer = Manufacturer.objects.create(name="Juniper", slug="juniper") + device_role = DeviceRole.objects.create(name="Firewall", slug="firewall") + device_type = DeviceType.objects.create(slug="srx3600", model="SRX3600", manufacturer=manufacturer) + + self.device = Device.objects.create( + device_type=device_type, name="device1", device_role=device_role, site=self.site + ) + + self.succeeded_task1 = OnboardingTask.objects.create( + ip_address="10.10.10.10", + site=self.site, + status=OnboardingStatusChoices.STATUS_SUCCEEDED, + created_device=self.device, + ) + + self.succeeded_task2 = OnboardingTask.objects.create( + ip_address="10.10.10.10", + site=self.site, + status=OnboardingStatusChoices.STATUS_SUCCEEDED, + created_device=self.device, + ) + + self.failed_task1 = OnboardingTask.objects.create( + ip_address="10.10.10.10", + site=self.site, + status=OnboardingStatusChoices.STATUS_FAILED, + created_device=self.device, + ) + + self.failed_task2 = OnboardingTask.objects.create( + ip_address="10.10.10.10", + site=self.site, + status=OnboardingStatusChoices.STATUS_FAILED, + created_device=self.device, + ) + + def test_onboardingdevice_autocreated(self): + """Verify that OnboardingDevice is auto-created.""" + onboarding_device = OnboardingDevice.objects.get(device=self.device) + self.assertEqual(self.device, onboarding_device.device) + + def test_last_check_attempt_date(self): + """Verify OnboardingDevice last attempt.""" + onboarding_device = OnboardingDevice.objects.get(device=self.device) + self.assertEqual(onboarding_device.last_check_attempt_date, self.failed_task2.created_on) + + def test_last_check_successful_date(self): + """Verify OnboardingDevice last success.""" + onboarding_device = OnboardingDevice.objects.get(device=self.device) + self.assertEqual(onboarding_device.last_check_successful_date, self.succeeded_task2.created_on) + + def test_status(self): + """Verify OnboardingDevice status.""" + onboarding_device = OnboardingDevice.objects.get(device=self.device) + self.assertEqual(onboarding_device.status, self.failed_task2.status) + + def test_last_ot(self): + """Verify OnboardingDevice last ot.""" + onboarding_device = OnboardingDevice.objects.get(device=self.device) + self.assertEqual(onboarding_device.last_ot, self.failed_task2) From 1c406ade0ab563902e20b5ec4db094c366ab7613 Mon Sep 17 00:00:00 2001 From: Marek Zbroch Date: Tue, 25 Aug 2020 13:04:06 +0200 Subject: [PATCH 2/2] skip onboarding --- netbox_onboarding/choices.py | 2 ++ netbox_onboarding/models.py | 11 ++++++++++- netbox_onboarding/worker.py | 11 +++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/netbox_onboarding/choices.py b/netbox_onboarding/choices.py index ebcd9f4..f79bcaf 100644 --- a/netbox_onboarding/choices.py +++ b/netbox_onboarding/choices.py @@ -22,12 +22,14 @@ class OnboardingStatusChoices(ChoiceSet): STATUS_PENDING = "pending" STATUS_RUNNING = "running" STATUS_SUCCEEDED = "succeeded" + STATUS_SKIPPED = "skipped" CHOICES = ( (STATUS_FAILED, "failed"), (STATUS_PENDING, "pending"), (STATUS_RUNNING, "running"), (STATUS_SUCCEEDED, "succeeded"), + (STATUS_SKIPPED, "skipped"), ) diff --git a/netbox_onboarding/models.py b/netbox_onboarding/models.py index 7627880..fb8d50d 100644 --- a/netbox_onboarding/models.py +++ b/netbox_onboarding/models.py @@ -14,8 +14,8 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.db import models -from dcim.models import Device from django.urls import reverse +from dcim.models import Device from .choices import OnboardingStatusChoices, OnboardingFailChoices from .release import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_29 @@ -113,3 +113,12 @@ def last_ot(self): except ValueError: return None + +@receiver(post_save, sender=Device) +def init_onboarding_for_new_device(sender, instance, created, **kwargs): # pylint: disable=unused-argument + """Register to create a OnboardingDevice object for each new Device Object using Django Signal. + + https://docs.djangoproject.com/en/3.0/ref/signals/#post-save + """ + if created: + OnboardingDevice.objects.create(device=instance) diff --git a/netbox_onboarding/worker.py b/netbox_onboarding/worker.py index 4ba4b31..494d73d 100644 --- a/netbox_onboarding/worker.py +++ b/netbox_onboarding/worker.py @@ -21,6 +21,7 @@ from .choices import OnboardingFailChoices from .choices import OnboardingStatusChoices from .exceptions import OnboardException +from .models import OnboardingDevice from .models import OnboardingTask from .onboard import OnboardingManager @@ -29,7 +30,7 @@ @job("default") -def onboard_device(task_id, credentials): # pylint: disable=R0915 +def onboard_device(task_id, credentials): # pylint: disable=too-many-statements """Process a single OnboardingTask instance.""" username = credentials.username password = credentials.password @@ -44,6 +45,12 @@ def onboard_device(task_id, credentials): # pylint: disable=R0915 try: if ot.ip_address: onboarded_device = Device.objects.get(primary_ip4__address__net_host=ot.ip_address) + + if OnboardingDevice.objects.filter(device=onboarded_device, enabled=False): + ot.status = OnboardingStatusChoices.STATUS_SKIPPED + + return dict(ok=True) + except Device.DoesNotExist as exc: logger.info("Getting device with IP lookup failed: %s", str(exc)) except Device.MultipleObjectsReturned as exc: @@ -80,7 +87,7 @@ def onboard_device(task_id, credentials): # pylint: disable=R0915 ot.save() onboarding_status = False - except Exception as exc: # pylint: disable=W0703 + except Exception as exc: # pylint: disable=broad-except if onboarded_device: ot.created_device = onboarded_device