Skip to content

Commit 986fe8b

Browse files
authored
Merge pull request #100 from networktocode/develop-2.0
Release 2.0
2 parents 9cdb6d2 + 3cd7b02 commit 986fe8b

39 files changed

+2320
-1070
lines changed

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ pip install ntc-netbox-plugin-onboarding
2626
systemctl restart netbox netbox-rq
2727
```
2828

29-
> The plugin is compatible with NetBox 2.8.1 and higher
30-
29+
> The ntc-netbox-plugin-onboarding v1.3 is compatible with NetBox 2.8
30+
31+
> The ntc-netbox-plugin-onboarding v2 is compatible with NetBox 2.8 and NetBox 2.9
32+
3133
To ensure NetBox Onboarding plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `ntc-netbox-plugin-onboarding` package:
3234

3335
```no-highlight
@@ -64,26 +66,38 @@ The plugin behavior can be controlled with the following list of settings
6466
- `default_device_role_color` string (default FF0000), color assigned to the device role if it needs to be created.
6567
- `default_management_interface` string (default "PLACEHOLDER"), name of the management interface that will be created, if one can't be identified on the device.
6668
- `default_management_prefix_length` integer ( default 0), length of the prefix that will be used for the management IP address, if the IP can't be found.
69+
- `skip_device_type_on_update` boolean (default False), If True, an existing NetBox device will not get its device type updated. If False, device type will be updated with one discovered on a device.
70+
- `skip_manufacturer_on_update` boolean (default False), If True, an existing NetBox device will not get its manufacturer updated. If False, manufacturer will be updated with one discovered on a device.
6771
- `platform_map` (dictionary), mapping of an **auto-detected** Netmiko platform to the **NetBox slug** name of your Platform. The dictionary should be in the format:
6872
```python
6973
{
70-
<Netmiko Platform>: <NetBox Slug>
74+
<Netmiko Platform>: <NetBox Slug>
75+
}
76+
```
77+
- `onboarding_extensions_map` (dictionary), mapping of a NAPALM driver name to the loadable Python module used as an onboarding extension. The dictionary should be in the format:
78+
```python
79+
{
80+
<Napalm Driver Name>: <Loadable Python Module>
7181
}
7282
```
83+
- `object_match_strategy` (string), defines the method for searching models. There are
84+
currently two strategies, strict and loose. Strict has to be a direct match, normally
85+
using a slug. Loose allows a range of search criteria to match a single object. If multiple
86+
objects are returned an error is raised.
7387

7488
## Usage
7589

7690
### Preparation
7791

78-
To work properly the plugin needs to know the Site, Platform, Device Type, Device Role of each
79-
device as well as its primary IP address or DNS Name. It's recommended to create these objects in
80-
NetBox ahead of time and to provide them when you want to start the onboarding process.
92+
To properly onboard a device, the plugin needs to only know the Site as well as device's primary IP address or DNS Name.
8193

8294
> For DNS Name Resolution to work, the instance of NetBox must be able to resolve the name of the
8395
> device to IP address.
8496

97+
Providing other attributes (`Platform`, `Device Type`, `Device Role`) is optional - if any of these attributes is provided, plugin will use provided value for the onboarded device.
8598
If `Platform`, `Device Type` and/or `Device Role` are not provided, the plugin will try to identify these information automatically and, based on the settings, it can create them in NetBox as needed.
86-
> If the Platform is provided, it must contains a valid Napalm driver available to the worker in Python
99+
> If the Platform is provided, it must point to an existing NetBox Platform. NAPALM driver of this platform will be used only if it is defined for the platform in NetBox.
100+
> To use a preferred NAPALM driver, either define it in NetBox per platform or in the plugins settings under `platform_map`
87101

88102
### Onboard a new device
89103

development/base_configuration.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,20 @@
114114

115115
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
116116
# https://docs.djangoproject.com/en/1.11/topics/logging/
117-
LOGGING = {}
117+
LOGGING = {
118+
"version": 1,
119+
"disable_existing_loggers": False,
120+
"formatters": {"rq_console": {"format": "%(asctime)s %(message)s", "datefmt": "%H:%M:%S",},},
121+
"handlers": {
122+
"rq_console": {
123+
"level": "DEBUG",
124+
"class": "rq.utils.ColorizingStreamHandler",
125+
"formatter": "rq_console",
126+
"exclude": ["%(asctime)s"],
127+
},
128+
},
129+
"loggers": {"rq.worker": {"handlers": ["rq_console"], "level": "DEBUG"},},
130+
}
118131

119132
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
120133
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Example of custom onboarding class.
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 netbox_onboarding.netbox_keeper import NetboxKeeper
16+
from netbox_onboarding.onboarding.onboarding import Onboarding
17+
18+
19+
class MyOnboardingClass(Onboarding):
20+
"""Custom onboarding class example.
21+
22+
Main purpose of this class is to access and modify the onboarding_kwargs.
23+
By accessing the onboarding kwargs, user gains ability to modify
24+
onboarding parameters before the objects are created in NetBox.
25+
26+
This class adds the get_device_role method that does the static
27+
string comparison and returns the device role.
28+
"""
29+
30+
def run(self, onboarding_kwargs):
31+
"""Ensures network device."""
32+
# Access hostname from onboarding_kwargs and get device role automatically
33+
device_new_role = self.get_device_role(hostname=onboarding_kwargs["netdev_hostname"])
34+
35+
# Update the device role in onboarding kwargs dictionary
36+
onboarding_kwargs["netdev_nb_role_slug"] = device_new_role
37+
38+
nb_k = NetboxKeeper(**onboarding_kwargs)
39+
nb_k.ensure_device()
40+
41+
self.created_device = nb_k.device
42+
43+
@staticmethod
44+
def get_device_role(hostname):
45+
"""Returns the device role based on hostname data.
46+
47+
This is a static analysis of hostname string content only
48+
"""
49+
hostname_lower = hostname.lower()
50+
if ("rtr" in hostname_lower) or ("router" in hostname_lower):
51+
role = "router"
52+
elif ("sw" in hostname_lower) or ("switch" in hostname_lower):
53+
role = "switch"
54+
elif ("fw" in hostname_lower) or ("firewall" in hostname_lower):
55+
role = "firewall"
56+
elif "dc" in hostname_lower:
57+
role = "datacenter"
58+
else:
59+
role = "generic"
60+
61+
return role
62+
63+
64+
class OnboardingDriverExtensions:
65+
"""This is an example of a custom onboarding driver extension.
66+
67+
This extension sets the onboarding_class to MyOnboardingClass,
68+
which is an example class of how to access and modify the device
69+
role automatically through the onboarding process.
70+
"""
71+
72+
def __init__(self, napalm_device):
73+
"""Inits the class."""
74+
self.napalm_device = napalm_device
75+
self.onboarding_class = MyOnboardingClass
76+
self.ext_result = None
77+
78+
def get_onboarding_class(self):
79+
"""Return onboarding class for IOS driver.
80+
81+
Currently supported is Standalone Onboarding Process
82+
83+
Result of this method is used by the OnboardingManager to
84+
initiate the instance of the onboarding class.
85+
"""
86+
return self.onboarding_class
87+
88+
def get_ext_result(self):
89+
"""This method is used to store any object as a return value.
90+
91+
Result of this method is passed to the onboarding class as
92+
driver_addon_result argument.
93+
94+
:return: Any()
95+
"""
96+
return self.ext_result

docs/release-notes/version-2.0.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ntc-netbox-plugin-onboarding v2.0 Release Notes
2+
3+
## v2.0
4+
5+
### Enhancements
6+
7+
* NetBox 2.9 support - Supported releases 2.8 and 2.9
8+
* Onboarding extensions - Customizable onboarding process through Python modules.
9+
* Onboarding details exposed in a device view - Date, Status, Last success and Latest task id related to the onboarded device are presented under the device view.
10+
* Onboarding task view - Onboarding details exposed in a dedicated view, including NetBox's ChangeLog.
11+
* Onboarding Changelog - Onboarding uses NetBox's ChangeLog to display user and changes made to the Onboarding Task object.
12+
* Skip onboarding feature - New attribute in the OnboardingDevice model allows to skip the onboarding request on devices with disabled onboarding setting.
13+
14+
### Bug Fixes
15+
16+
* Fixed race condition in `worker.py`
17+
* Improved logging
18+
19+
### Additional Changes
20+
21+
* Platform map now includes NAPALM drivers as defined in NetBox
22+
* Tests have been refactored to inherit NetBox's tests
23+
* Onboarding process will update the Device found by the IP-address lookup. In case of no existing device with onboarded IP-address is found in NetBox, onboarding might update the existing NetBox' looking up by network device's hostname.
24+
* Onboarding will raise Exception when `create_device_type_if_missing` is set to `False` for existing Device with DeviceType mismatch (behaviour pre https://github.com/networktocode/ntc-netbox-plugin-onboarding/issues/74)

netbox_onboarding/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
limitations under the License.
1313
"""
1414

15-
__version__ = "1.3.0"
15+
__version__ = "2.0.0"
1616

1717
from extras.plugins import PluginConfig
1818

@@ -39,7 +39,11 @@ class OnboardingConfig(PluginConfig):
3939
"default_management_prefix_length": 0,
4040
"default_device_status": "active",
4141
"create_management_interface_if_missing": True,
42+
"skip_device_type_on_update": False,
43+
"skip_manufacturer_on_update": False,
4244
"platform_map": {},
45+
"onboarding_extensions_map": {"ios": "netbox_onboarding.onboarding_extensions.ios",},
46+
"object_match_strategy": "loose",
4347
}
4448
caching_config = {}
4549

netbox_onboarding/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ class OnboardingTaskAdmin(admin.ModelAdmin):
3232
"failed_reason",
3333
"port",
3434
"timeout",
35-
"created_on",
35+
"created",
3636
)

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

netbox_onboarding/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Constants for netbox_onboarding plugin."""
2-
NETMIKO_TO_NAPALM = {
2+
3+
NETMIKO_TO_NAPALM_STATIC = {
34
"cisco_ios": "ios",
45
"cisco_nxos": "nxos_ssh",
56
"arista_eos": "eos",

netbox_onboarding/exceptions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Exceptions.
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+
16+
class OnboardException(Exception):
17+
"""A failure occurred during the onboarding process.
18+
19+
The exception includes a reason "slug" as defined below as well as a humanized message.
20+
"""
21+
22+
REASONS = (
23+
"fail-config", # config provided is not valid
24+
"fail-connect", # device is unreachable at IP:PORT
25+
"fail-execute", # unable to execute device/API command
26+
"fail-login", # bad username/password
27+
"fail-dns", # failed to get IP address from name resolution
28+
"fail-general", # other error
29+
)
30+
31+
def __init__(self, reason, message, **kwargs):
32+
"""Exception Init."""
33+
super(OnboardException, self).__init__(kwargs)
34+
self.reason = reason
35+
self.message = message
36+
37+
def __str__(self):
38+
"""Exception __str__."""
39+
return f"{self.__class__.__name__}: {self.reason}: {self.message}"

netbox_onboarding/filters.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ class OnboardingTaskFilter(NameSlugSearchFilterSet):
2626

2727
q = django_filters.CharFilter(method="search", label="Search",)
2828

29-
site_id = django_filters.ModelMultipleChoiceFilter(queryset=Site.objects.all(), label="Site (ID)",)
30-
3129
site = django_filters.ModelMultipleChoiceFilter(
3230
field_name="site__slug", queryset=Site.objects.all(), to_field_name="slug", label="Site (slug)",
3331
)
@@ -46,7 +44,7 @@ class Meta: # noqa: D106 "Missing docstring in public nested class"
4644
model = OnboardingTask
4745
fields = ["id", "site", "site_id", "platform", "role", "status", "failed_reason"]
4846

49-
def search(self, queryset, name, value):
47+
def search(self, queryset, name, value): # pylint: disable=unused-argument, no-self-use
5048
"""Perform the filtered search."""
5149
if not value.strip():
5250
return queryset
@@ -55,7 +53,7 @@ def search(self, queryset, name, value):
5553
| Q(ip_address__icontains=value)
5654
| Q(site__name__icontains=value)
5755
| Q(platform__name__icontains=value)
58-
| Q(device__icontains=value)
56+
| Q(created_device__name__icontains=value)
5957
| Q(status__icontains=value)
6058
| Q(failed_reason__icontains=value)
6159
| Q(message__icontains=value)

netbox_onboarding/forms.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
"""
1414

1515
from django import forms
16+
from django.db import transaction
1617
from django_rq import get_queue
1718

18-
from utilities.forms import BootstrapMixin
19+
from utilities.forms import BootstrapMixin, CSVModelForm
1920
from dcim.models import Site, Platform, DeviceRole, DeviceType
20-
from extras.forms import CustomFieldModelCSVForm
2121

2222
from .models import OnboardingTask
2323
from .choices import OnboardingStatusChoices, OnboardingFailChoices
@@ -33,7 +33,7 @@ class OnboardingTaskForm(BootstrapMixin, forms.ModelForm):
3333
required=True, label="IP address", help_text="IP Address/DNS Name of the device to onboard"
3434
)
3535

36-
site = forms.ModelChoiceField(required=True, queryset=Site.objects.all(), to_field_name="slug")
36+
site = forms.ModelChoiceField(required=True, queryset=Site.objects.all())
3737

3838
username = forms.CharField(required=False, help_text="Device username (will not be stored in database)")
3939
password = forms.CharField(
@@ -106,7 +106,7 @@ class Meta: # noqa: D106 "Missing docstring in public nested class"
106106
fields = ["q", "site", "platform", "status", "failed_reason"]
107107

108108

109-
class OnboardingTaskFeedCSVForm(CustomFieldModelCSVForm):
109+
class OnboardingTaskFeedCSVForm(CSVModelForm):
110110
"""Form for entering CSV to bulk-import OnboardingTask entries."""
111111

112112
site = forms.ModelChoiceField(
@@ -149,12 +149,21 @@ class OnboardingTaskFeedCSVForm(CustomFieldModelCSVForm):
149149

150150
class Meta: # noqa: D106 "Missing docstring in public nested class"
151151
model = OnboardingTask
152-
fields = OnboardingTask.csv_headers
152+
fields = [
153+
"site",
154+
"ip_address",
155+
"port",
156+
"timeout",
157+
"platform",
158+
"role",
159+
]
153160

154161
def save(self, commit=True, **kwargs):
155162
"""Save the model, and add it and the associated credentials to the onboarding worker queue."""
156163
model = super().save(commit=commit, **kwargs)
157164
if commit:
158165
credentials = Credentials(self.data.get("username"), self.data.get("password"), self.data.get("secret"))
159-
get_queue("default").enqueue("netbox_onboarding.worker.onboard_device", model.pk, credentials)
166+
transaction.on_commit(
167+
lambda: get_queue("default").enqueue("netbox_onboarding.worker.onboard_device", model.pk, credentials)
168+
)
160169
return model

netbox_onboarding/metrics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Plugin additions to the NetBox navigation menu.
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 prometheus_client import Counter
15+
16+
onboardingtask_results_counter = Counter(
17+
name="onboardingtask_results_total", documentation="Count of results for Onboarding Task", labelnames=("status",)
18+
)

0 commit comments

Comments
 (0)