Skip to content

Commit 2704750

Browse files
Merge pull request #55 from jvanderaa/jv_platform_mappings
Support Platform Mapping in Settings
2 parents a31dde7 + 7a143a1 commit 2704750

9 files changed

+145
-7
lines changed

.bandit.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
skips: []

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ The plugin behavior can be controlled with the following list of settings
5353
- `default_device_role_color` string (default FF0000), color assigned to the device role if it needs to be created.
5454
- `default_management_interface` string (default "PLACEHOLDER"), name of the management interface that will be created, if one can't be identified on the device.
5555
- `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.
56+
- `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:
57+
```python
58+
{
59+
<Netmiko Platform>: <NetBox Slug>
60+
}
61+
```
5662

5763
## Usage
5864

development/base_configuration.py

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
# Enable installed plugins. Add the name of each plugin to the list.
156156
PLUGINS = ["netbox_onboarding"]
157157

158+
PLUGINS_CONFIG = {"netbox_onboarding": {}}
158159
# Plugins configuration settings. These settings are used by various plugins that the user may have installed.
159160
# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
160161
# PLUGINS_CONFIG = {}

development/docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ services:
1919
volumes:
2020
- ./base_configuration.py:/opt/netbox/netbox/netbox/base_configuration.py
2121
- ./netbox_${NETBOX_VER}/configuration.py:/opt/netbox/netbox/netbox/configuration.py
22-
- ../netbox_onboarding:/source/netbox_onboarding
22+
- ../:/source
2323
tty: true
2424
worker:
2525
build:

netbox_onboarding/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class OnboardingConfig(PluginConfig):
3939
"default_management_prefix_length": 0,
4040
"default_device_status": "active",
4141
"create_management_interface_if_missing": True,
42+
"platform_map": {},
4243
}
4344
caching_config = {}
4445

netbox_onboarding/onboard.py

+42-4
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,25 @@ def check_reachability(self):
125125
raise OnboardException(reason="fail-connect", message=f"ERROR device unreachable: {ip_addr}:{port}")
126126

127127
@staticmethod
128-
def guess_netmiko_device_type(**kwargs):
128+
def check_netmiko_conversion(guessed_device_type, platform_map=None):
129+
"""Method to convert Netmiko device type into the mapped type if defined in the settings file.
130+
131+
Args:
132+
guessed_device_type (string): Netmiko device type guessed platform
133+
test_platform_map (dict): Platform Map for use in testing
134+
135+
Returns:
136+
string: Platform name
137+
"""
138+
# If this is defined, process the mapping
139+
if platform_map:
140+
# Attempt to get a mapped slug. If there is no slug, return the guessed_device_type as the slug
141+
return platform_map.get(guessed_device_type, guessed_device_type)
142+
143+
# There is no mapping configured, return what was brought in
144+
return guessed_device_type
145+
146+
def guess_netmiko_device_type(self, **kwargs):
129147
"""Guess the device type of host, based on Netmiko."""
130148
guessed_device_type = None
131149

@@ -157,7 +175,8 @@ def guess_netmiko_device_type(**kwargs):
157175

158176
logging.info("INFO device type is %s", guessed_device_type)
159177

160-
return guessed_device_type
178+
# Get the platform map from the PLUGIN SETTINGS, Return the result of doing a check_netmiko_conversion
179+
return self.check_netmiko_conversion(guessed_device_type, platform_map=PLUGIN_SETTINGS.get("platform_map", {}))
161180

162181
def get_platform_slug(self):
163182
"""Get platform slug in netmiko format (ie cisco_ios, cisco_xr etc)."""
@@ -173,17 +192,30 @@ def get_platform_slug(self):
173192
return platform_slug
174193

175194
@staticmethod
176-
def get_platform_object_from_netbox(platform_slug):
195+
def get_platform_object_from_netbox(
196+
platform_slug, create_platform_if_missing=PLUGIN_SETTINGS["create_platform_if_missing"]
197+
):
177198
"""Get platform object from NetBox filtered by platform_slug.
178199
200+
Args:
201+
platform_slug (string): slug of a platform object present in NetBox, object will be created if not present
202+
and create_platform_if_missing is enabled
203+
204+
Return:
205+
dcim.models.Platform object
206+
207+
Raises:
208+
OnboardException
209+
179210
Lookup is performed based on the object's slug field (not the name field)
180211
"""
181212
try:
213+
# Get the platform from the NetBox DB
182214
platform = Platform.objects.get(slug=platform_slug)
183215
logging.info("PLATFORM: found in NetBox %s", platform_slug)
184216
except Platform.DoesNotExist:
185217

186-
if not PLUGIN_SETTINGS["create_platform_if_missing"]:
218+
if not create_platform_if_missing:
187219
raise OnboardException(
188220
reason="fail-general", message=f"ERROR platform not found in NetBox: {platform_slug}"
189221
)
@@ -199,6 +231,12 @@ def get_platform_object_from_netbox(platform_slug):
199231
)
200232
platform.save()
201233

234+
else:
235+
if not platform.napalm_driver:
236+
raise OnboardException(
237+
reason="fail-general", message=f"ERROR platform is missing the NAPALM Driver: {platform_slug}",
238+
)
239+
202240
return platform
203241

204242
def check_ip(self):

netbox_onboarding/tests/test_onboard.py netbox_onboarding/tests/test_netbox_keeper.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def setUp(self):
3232

3333
self.manufacturer1 = Manufacturer.objects.create(name="Juniper", slug="juniper")
3434
self.platform1 = Platform.objects.create(name="JunOS", slug="junos")
35+
self.platform2 = Platform.objects.create(name="Cisco NX-OS", slug="cisco-nx-os")
3536
self.device_type1 = DeviceType.objects.create(slug="srx3600", model="SRX3600", manufacturer=self.manufacturer1)
3637
self.device_role1 = DeviceRole.objects.create(name="Firewall", slug="firewall")
3738

@@ -45,10 +46,12 @@ def setUp(self):
4546
self.onboarding_task4 = OnboardingTask.objects.create(
4647
ip_address="ntc123.local", site=self.site1, role=self.device_role1, platform=self.platform1
4748
)
48-
4949
self.onboarding_task5 = OnboardingTask.objects.create(
5050
ip_address="bad.local", site=self.site1, role=self.device_role1, platform=self.platform1
5151
)
52+
self.onboarding_task6 = OnboardingTask.objects.create(
53+
ip_address="192.0.2.2", site=self.site1, role=self.device_role1, platform=self.platform2
54+
)
5255
self.onboarding_task7 = OnboardingTask.objects.create(
5356
ip_address="192.0.2.1/32", site=self.site1, role=self.device_role1, platform=self.platform1
5457
)
@@ -229,3 +232,32 @@ def test_failed_check_ip(self, mock_get_hostbyname):
229232
ndk7.check_ip()
230233
self.assertEqual(exc_info.exception.reason, "fail-prefix")
231234
self.assertEqual(exc_info.exception.message, "ERROR appears a prefix was entered: 192.0.2.1/32")
235+
236+
def test_platform_map(self):
237+
"""Verify platform mapping of netmiko to slug functionality."""
238+
# Create static mapping
239+
platform_map = {"cisco_ios": "ios", "arista_eos": "eos", "cisco_nxos": "cisco-nxos"}
240+
241+
# Generate an instance of a Cisco IOS device with the mapping defined
242+
self.ndk1 = NetdevKeeper(self.onboarding_task1)
243+
244+
#
245+
# Test positive assertions
246+
#
247+
248+
# Test Cisco_ios
249+
self.assertEqual(self.ndk1.check_netmiko_conversion("cisco_ios", platform_map=platform_map), "ios")
250+
# Test Arista EOS
251+
self.assertEqual(self.ndk1.check_netmiko_conversion("arista_eos", platform_map=platform_map), "eos")
252+
# Test cisco_nxos
253+
self.assertEqual(self.ndk1.check_netmiko_conversion("cisco_nxos", platform_map=platform_map), "cisco-nxos")
254+
255+
#
256+
# Test Negative assertion
257+
#
258+
259+
# Test a non-converting item
260+
self.assertEqual(
261+
self.ndk1.check_netmiko_conversion("cisco-device-platform", platform_map=platform_map),
262+
"cisco-device-platform",
263+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Unit tests for netbox_onboarding.onboard module and its classes.
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 Platform
17+
from netbox_onboarding.onboard import NetdevKeeper, OnboardException
18+
19+
20+
class NetdevKeeperTestCase(TestCase):
21+
"""Test the NetdevKeeper Class."""
22+
23+
def setUp(self):
24+
"""Create a superuser and token for API calls."""
25+
self.platform1 = Platform.objects.create(name="JunOS", slug="junos", napalm_driver="junos")
26+
self.platform2 = Platform.objects.create(name="Cisco NX-OS", slug="cisco-nx-os")
27+
28+
def test_get_platform_object_from_netbox(self):
29+
"""Test of platform object from netbox."""
30+
# Test assigning platform
31+
platform = NetdevKeeper.get_platform_object_from_netbox("junos", create_platform_if_missing=False)
32+
self.assertIsInstance(platform, Platform)
33+
34+
# Test creation of missing platform object
35+
platform = NetdevKeeper.get_platform_object_from_netbox("arista_eos", create_platform_if_missing=True)
36+
self.assertIsInstance(platform, Platform)
37+
self.assertEqual(platform.napalm_driver, "eos")
38+
39+
# Test failed unable to find the device and not part of the NETMIKO TO NAPALM keys
40+
with self.assertRaises(OnboardException) as exc_info:
41+
platform = NetdevKeeper.get_platform_object_from_netbox("notthere", create_platform_if_missing=True)
42+
self.assertEqual(
43+
exc_info.exception.message,
44+
"ERROR platform not found in NetBox and it's eligible for auto-creation: notthere",
45+
)
46+
self.assertEqual(exc_info.exception.reason, "fail-general")
47+
48+
# Test searching for an object, does not exist, but create_platform is false
49+
with self.assertRaises(OnboardException) as exc_info:
50+
platform = NetdevKeeper.get_platform_object_from_netbox("cisco_ios", create_platform_if_missing=False)
51+
self.assertEqual(exc_info.exception.message, "ERROR platform not found in NetBox: cisco_ios")
52+
self.assertEqual(exc_info.exception.reason, "fail-general")
53+
54+
# Test NAPALM Driver not defined in NetBox
55+
with self.assertRaises(OnboardException) as exc_info:
56+
platform = NetdevKeeper.get_platform_object_from_netbox("cisco-nx-os", create_platform_if_missing=False)
57+
self.assertEqual(exc_info.exception.message, "ERROR platform is missing the NAPALM Driver: cisco-nx-os")
58+
self.assertEqual(exc_info.exception.reason, "fail-general")

tasks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def bandit(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER):
283283
"""
284284
docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox"
285285
context.run(
286-
f'{docker} sh -c "cd /source && bandit --recursive ./"',
286+
f'{docker} sh -c "cd /source && bandit --configfile .bandit.yml --recursive ./"',
287287
env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver},
288288
pty=True,
289289
)

0 commit comments

Comments
 (0)