Skip to content

Commit 1eeea6f

Browse files
authored
Merge pull request #83 from networktocode/mzb-develop-2.0-worker-status
Expose onboarding details in device view
2 parents c0159ee + 1c406ad commit 1eeea6f

File tree

7 files changed

+257
-2
lines changed

7 files changed

+257
-2
lines changed

netbox_onboarding/choices.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ class OnboardingStatusChoices(ChoiceSet):
2222
STATUS_PENDING = "pending"
2323
STATUS_RUNNING = "running"
2424
STATUS_SUCCEEDED = "succeeded"
25+
STATUS_SKIPPED = "skipped"
2526

2627
CHOICES = (
2728
(STATUS_FAILED, "failed"),
2829
(STATUS_PENDING, "pending"),
2930
(STATUS_RUNNING, "running"),
3031
(STATUS_SUCCEEDED, "succeeded"),
32+
(STATUS_SKIPPED, "skipped"),
3133
)
3234

3335

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 2.2.10 on 2020-08-21 11:05
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("netbox_onboarding", "0001_initial"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="OnboardingDevice",
16+
fields=[
17+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False)),
18+
("enabled", models.BooleanField(default=True)),
19+
("device", models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="dcim.Device")),
20+
],
21+
),
22+
]

netbox_onboarding/models.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
See the License for the specific language governing permissions and
1212
limitations under the License.
1313
"""
14+
from django.db.models.signals import post_save
15+
from django.dispatch import receiver
1416
from django.db import models
1517
from django.urls import reverse
18+
from dcim.models import Device
1619
from .choices import OnboardingStatusChoices, OnboardingFailChoices
1720
from .release import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_29
1821

@@ -64,3 +67,58 @@ def get_absolute_url(self):
6467
from utilities.querysets import RestrictedQuerySet # pylint: disable=no-name-in-module, import-outside-toplevel
6568

6669
objects = RestrictedQuerySet.as_manager()
70+
71+
72+
class OnboardingDevice(models.Model):
73+
"""The status of each Onboarded Device is tracked in the OnboardingDevice table."""
74+
75+
device = models.OneToOneField(to="dcim.Device", on_delete=models.CASCADE)
76+
enabled = models.BooleanField(default=True, help_text="Whether (re)onboarding of this device is permitted")
77+
78+
@property
79+
def last_check_attempt_date(self):
80+
"""Date of last onboarding attempt for a device."""
81+
try:
82+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").created_on
83+
except ValueError:
84+
return "unknown"
85+
86+
@property
87+
def last_check_successful_date(self):
88+
"""Date of last successful onboarding for a device."""
89+
try:
90+
return (
91+
OnboardingTask.objects.filter(
92+
created_device=self.device, status=OnboardingStatusChoices.STATUS_SUCCEEDED
93+
)
94+
.latest("created_on")
95+
.created_on
96+
)
97+
except ValueError:
98+
return "unknown"
99+
100+
@property
101+
def status(self):
102+
"""Last onboarding status."""
103+
try:
104+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").status
105+
except ValueError:
106+
return "unknown"
107+
108+
@property
109+
def last_ot(self):
110+
"""Last onboarding task."""
111+
try:
112+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on")
113+
except ValueError:
114+
return None
115+
116+
117+
@receiver(post_save, sender=Device)
118+
def init_onboarding_for_new_device(sender, instance, created, **kwargs): # pylint: disable=unused-argument
119+
"""Register to create a OnboardingDevice object for each new Device Object using Django Signal.
120+
121+
https://docs.djangoproject.com/en/3.0/ref/signals/#post-save
122+
"""
123+
if created:
124+
OnboardingDevice.objects.create(device=instance)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Onboarding template content.
2+
3+
(c) 2020 Network To Code
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
"""
14+
15+
from extras.plugins import PluginTemplateExtension
16+
from .models import OnboardingDevice
17+
18+
19+
class DeviceContent(PluginTemplateExtension): # pylint: disable=abstract-method
20+
"""Table to show onboarding details on Device objects."""
21+
22+
model = "dcim.device"
23+
24+
def right_page(self):
25+
"""Show table on right side of view."""
26+
onboarding = OnboardingDevice.objects.filter(device=self.context["object"]).first()
27+
28+
if not onboarding.enabled:
29+
return None
30+
31+
status = onboarding.status
32+
last_check_attempt_date = onboarding.last_check_attempt_date
33+
last_check_successful_date = onboarding.last_check_successful_date
34+
last_ot = onboarding.last_ot
35+
36+
return self.render(
37+
"netbox_onboarding/device_onboarding_table.html",
38+
extra_context={
39+
"status": status,
40+
"last_check_attempt_date": last_check_attempt_date,
41+
"last_check_successful_date": last_check_successful_date,
42+
"last_ot": last_ot,
43+
},
44+
)
45+
46+
47+
template_extensions = [DeviceContent]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% block content %}
2+
<div class="panel panel-default">
3+
<div class="panel-heading">
4+
<strong>Device Onboarding</strong>
5+
</div>
6+
<table class="table table-hover panel-body">
7+
<tbody>
8+
<tr>
9+
<th>Date</th>
10+
<th>Status</th>
11+
<th>Date of last success</th>
12+
<th>Latest Task</th>
13+
</tr>
14+
<tr>
15+
<td>
16+
{{ last_check_attempt_date }}
17+
</td>
18+
<td>
19+
{{ status }}
20+
</td>
21+
<td>
22+
{{ last_check_successful_date }}
23+
</td>
24+
<td>
25+
{{ last_ot.pk }}
26+
</td>
27+
</tr>
28+
</tbody>
29+
</table>
30+
</div>
31+
{% endblock %}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Unit tests for netbox_onboarding OnboardingDevice model.
2+
3+
(c) 2020 Network To Code
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
"""
14+
from django.test import TestCase
15+
16+
from dcim.models import Site, DeviceRole, DeviceType, Manufacturer, Device
17+
18+
from netbox_onboarding.models import OnboardingTask
19+
from netbox_onboarding.models import OnboardingDevice
20+
from netbox_onboarding.choices import OnboardingStatusChoices
21+
22+
23+
class OnboardingDeviceModelTestCase(TestCase):
24+
"""Test the Onboarding models."""
25+
26+
def setUp(self):
27+
"""Setup objects for Onboarding Model tests."""
28+
self.site = Site.objects.create(name="USWEST", slug="uswest")
29+
manufacturer = Manufacturer.objects.create(name="Juniper", slug="juniper")
30+
device_role = DeviceRole.objects.create(name="Firewall", slug="firewall")
31+
device_type = DeviceType.objects.create(slug="srx3600", model="SRX3600", manufacturer=manufacturer)
32+
33+
self.device = Device.objects.create(
34+
device_type=device_type, name="device1", device_role=device_role, site=self.site
35+
)
36+
37+
self.succeeded_task1 = OnboardingTask.objects.create(
38+
ip_address="10.10.10.10",
39+
site=self.site,
40+
status=OnboardingStatusChoices.STATUS_SUCCEEDED,
41+
created_device=self.device,
42+
)
43+
44+
self.succeeded_task2 = OnboardingTask.objects.create(
45+
ip_address="10.10.10.10",
46+
site=self.site,
47+
status=OnboardingStatusChoices.STATUS_SUCCEEDED,
48+
created_device=self.device,
49+
)
50+
51+
self.failed_task1 = OnboardingTask.objects.create(
52+
ip_address="10.10.10.10",
53+
site=self.site,
54+
status=OnboardingStatusChoices.STATUS_FAILED,
55+
created_device=self.device,
56+
)
57+
58+
self.failed_task2 = OnboardingTask.objects.create(
59+
ip_address="10.10.10.10",
60+
site=self.site,
61+
status=OnboardingStatusChoices.STATUS_FAILED,
62+
created_device=self.device,
63+
)
64+
65+
def test_onboardingdevice_autocreated(self):
66+
"""Verify that OnboardingDevice is auto-created."""
67+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
68+
self.assertEqual(self.device, onboarding_device.device)
69+
70+
def test_last_check_attempt_date(self):
71+
"""Verify OnboardingDevice last attempt."""
72+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
73+
self.assertEqual(onboarding_device.last_check_attempt_date, self.failed_task2.created_on)
74+
75+
def test_last_check_successful_date(self):
76+
"""Verify OnboardingDevice last success."""
77+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
78+
self.assertEqual(onboarding_device.last_check_successful_date, self.succeeded_task2.created_on)
79+
80+
def test_status(self):
81+
"""Verify OnboardingDevice status."""
82+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
83+
self.assertEqual(onboarding_device.status, self.failed_task2.status)
84+
85+
def test_last_ot(self):
86+
"""Verify OnboardingDevice last ot."""
87+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
88+
self.assertEqual(onboarding_device.last_ot, self.failed_task2)

netbox_onboarding/worker.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .choices import OnboardingFailChoices
2222
from .choices import OnboardingStatusChoices
2323
from .exceptions import OnboardException
24+
from .models import OnboardingDevice
2425
from .models import OnboardingTask
2526
from .onboard import OnboardingManager
2627

@@ -29,7 +30,7 @@
2930

3031

3132
@job("default")
32-
def onboard_device(task_id, credentials): # pylint: disable=R0915
33+
def onboard_device(task_id, credentials): # pylint: disable=too-many-statements
3334
"""Process a single OnboardingTask instance."""
3435
username = credentials.username
3536
password = credentials.password
@@ -44,6 +45,12 @@ def onboard_device(task_id, credentials): # pylint: disable=R0915
4445
try:
4546
if ot.ip_address:
4647
onboarded_device = Device.objects.get(primary_ip4__address__net_host=ot.ip_address)
48+
49+
if OnboardingDevice.objects.filter(device=onboarded_device, enabled=False):
50+
ot.status = OnboardingStatusChoices.STATUS_SKIPPED
51+
52+
return dict(ok=True)
53+
4754
except Device.DoesNotExist as exc:
4855
logger.info("Getting device with IP lookup failed: %s", str(exc))
4956
except Device.MultipleObjectsReturned as exc:
@@ -80,7 +87,7 @@ def onboard_device(task_id, credentials): # pylint: disable=R0915
8087
ot.save()
8188
onboarding_status = False
8289

83-
except Exception as exc: # pylint: disable=W0703
90+
except Exception as exc: # pylint: disable=broad-except
8491
if onboarded_device:
8592
ot.created_device = onboarded_device
8693

0 commit comments

Comments
 (0)