Skip to content

Commit 26609f4

Browse files
committed
expose onboarding details in device view
1 parent 813ff83 commit 26609f4

File tree

5 files changed

+247
-1
lines changed

5 files changed

+247
-1
lines changed
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: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""OnboardingTask Django model.
1+
"""OnboardingTask models.
22
33
(c) 2020 Network To Code
44
Licensed under the Apache License, Version 2.0 (the "License");
@@ -11,10 +11,23 @@
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
17+
from dcim.models import Device
1518
from .choices import OnboardingStatusChoices, OnboardingFailChoices
1619

1720

21+
@receiver(post_save, sender=Device)
22+
def init_onboarding_for_new_device(sender, instance, created, **kwargs): # pylint: disable=W0613
23+
"""Register to create a OnboardingDevice object for each new Device Object using Django Signal.
24+
25+
https://docs.djangoproject.com/en/3.0/ref/signals/#post-save
26+
"""
27+
if created:
28+
OnboardingDevice.objects.create(device=instance)
29+
30+
1831
class OnboardingTask(models.Model):
1932
"""The status of each onboarding Task is tracked in the OnboardingTask table."""
2033

@@ -62,3 +75,48 @@ class Meta: # noqa: D106 "missing docstring in public nested class"
6275
def __str__(self):
6376
"""String representation of an OnboardingTask."""
6477
return f"{self.site} : {self.ip_address}"
78+
79+
80+
class OnboardingDevice(models.Model):
81+
"""The status of each Onboarded Device is tracked in the OnboardingDevice table."""
82+
83+
device = models.OneToOneField(to="dcim.Device", on_delete=models.CASCADE)
84+
enabled = models.BooleanField(default=True)
85+
86+
@property
87+
def last_check_attempt_date(self):
88+
"""Date of last onboarding attempt for a device."""
89+
try:
90+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").created_on
91+
except ValueError:
92+
return "unknown"
93+
94+
@property
95+
def last_check_successfull_date(self):
96+
"""Date of last successfull onboarding for a device."""
97+
try:
98+
return (
99+
OnboardingTask.objects.filter(
100+
created_device=self.device, status=OnboardingStatusChoices.STATUS_SUCCEEDED
101+
)
102+
.latest("created_on")
103+
.created_on
104+
)
105+
except ValueError:
106+
return "unknown"
107+
108+
@property
109+
def status(self):
110+
"""Last onboarding status."""
111+
try:
112+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on").status
113+
except ValueError:
114+
return "unknown"
115+
116+
@property
117+
def last_ot(self):
118+
"""Last onboarding task."""
119+
try:
120+
return OnboardingTask.objects.filter(created_device=self.device).latest("created_on")
121+
except ValueError:
122+
return None

netbox_onboarding/template_content.py

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_successfull_date = onboarding.last_check_successfull_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_successfull_date": last_check_successfull_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_successfull_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_successfull_date(self):
76+
"""Verify OnboardingDevice last success."""
77+
onboarding_device = OnboardingDevice.objects.get(device=self.device)
78+
self.assertEqual(onboarding_device.last_check_successfull_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)

0 commit comments

Comments
 (0)