Skip to content

Commit a31dde7

Browse files
authored
Merge pull request #57 from jvanderaa/jv_fqdn
Add DNS Based OnBoarding
2 parents ee0de9b + 0413727 commit a31dde7

File tree

6 files changed

+111
-7
lines changed

6 files changed

+111
-7
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,24 @@ The plugin behavior can be controlled with the following list of settings
4646
- `create_platform_if_missing` boolean (default True), If True, a new platform object will be created if the platform discovered by netmiko do not already exist and is in the list of supported platforms (`cisco_ios`, `cisco_nxos`, `arista_eos`, `juniper_junos`, `cisco_xr`)
4747
- `create_device_type_if_missing` boolean (default True), If True, a new device type object will be created if the model discovered by Napalm do not match an existing device type.
4848
- `create_manufacturer_if_missing` boolean (default True), If True, a new manufacturer object will be created if the manufacturer discovered by Napalm is do not match an existing manufacturer, this option is only valid if `create_device_type_if_missing` is True as well.
49-
- `create_device_role_if_missing` boolean (default True), If True, a new device role object will be created if the device role provided was not provided as part of the onboarding and if the `default_device_role` do not already exist.
49+
- `create_device_role_if_missing` boolean (default True), If True, a new device role object will be created if the device role provided was not provided as part of the onboarding and if the `default_device_role` do not already exist.
50+
- `create_management_interface_if_missing` boolean (default True), If True, add management interface and IP address to the device. If False no management interfaces will be created, nor will the IP address be added to NetBox, while the device will still get added.
5051
- `default_device_status` string (default "active"), status assigned to a new device by default (must be lowercase).
5152
- `default_device_role` string (default "network")
5253
- `default_device_role_color` string (default FF0000), color assigned to the device role if it needs to be created.
5354
- `default_management_interface` string (default "PLACEHOLDER"), name of the management interface that will be created, if one can't be identified on the device.
5455
- `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.
5556

5657
## Usage
58+
5759
### Preparation
5860

59-
To work properly the plugin needs to know the Site, Platform, Device Type, Device Role of each device as well as its primary IP address.
60-
It's recommended to create these objects in NetBox ahead of time and to provide them when you want to start the onboarding process.
61+
To work properly the plugin needs to know the Site, Platform, Device Type, Device Role of each
62+
device as well as its primary IP address or DNS Name. It's recommended to create these objects in
63+
NetBox ahead of time and to provide them when you want to start the onboarding process.
64+
65+
> For DNS Name Resolution to work, the instance of NetBox must be able to resolve the name of the
66+
> device to IP address.
6167
6268
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.
6369
> If the Platform is provided, it must contains a valid Napalm driver available to the worker in Python

netbox_onboarding/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class OnboardingConfig(PluginConfig):
3838
"default_management_interface": "PLACEHOLDER",
3939
"default_management_prefix_length": 0,
4040
"default_device_status": "active",
41+
"create_management_interface_if_missing": True,
4142
}
4243
caching_config = {}
4344

netbox_onboarding/choices.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ class OnboardingFailChoices(ChoiceSet):
3939
FAIL_CONNECT = "fail-connect"
4040
FAIL_EXECUTE = "fail-execute"
4141
FAIL_GENERAL = "fail-general"
42+
FAIL_DNS = "fail-dns"
4243

4344
CHOICES = (
4445
(FAIL_LOGIN, "fail-login"),
4546
(FAIL_CONFIG, "fail-config"),
4647
(FAIL_CONNECT, "fail-connect"),
4748
(FAIL_EXECUTE, "fail-execute"),
4849
(FAIL_GENERAL, "fail-general"),
50+
(FAIL_DNS, "fail-dns"),
4951
)

netbox_onboarding/forms.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
class OnboardingTaskForm(BootstrapMixin, forms.ModelForm):
3030
"""Form for creating a new OnboardingTask instance."""
3131

32-
ip_address = forms.CharField(required=True, label="IP address", help_text="IP address of the device to onboard")
32+
ip_address = forms.CharField(
33+
required=True, label="IP address", help_text="IP Address/DNS Name of the device to onboard"
34+
)
3335

3436
site = forms.ModelChoiceField(required=True, queryset=Site.objects.all(), to_field_name="slug")
3537

netbox_onboarding/onboard.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
from napalm import get_network_driver
2020
from napalm.base.exceptions import ConnectionException, CommandErrorException
21+
import netaddr
22+
from netaddr.core import AddrFormatError
2123

2224
from django.conf import settings
2325
from django.utils.text import slugify
@@ -48,6 +50,7 @@ class OnboardException(Exception):
4850
"fail-connect", # device is unreachable at IP:PORT
4951
"fail-execute", # unable to execute device/API command
5052
"fail-login", # bad username/password
53+
"fail-dns", # failed to get IP address from name resolution
5154
"fail-general", # other error
5255
)
5356

@@ -198,6 +201,46 @@ def get_platform_object_from_netbox(platform_slug):
198201

199202
return platform
200203

204+
def check_ip(self):
205+
"""Method to check if the IP address form field was an IP address.
206+
207+
If it is a DNS name, attempt to resolve the DNS address and assign the IP address to the
208+
name.
209+
210+
Returns:
211+
(bool): True if the IP address is an IP address, or a DNS entry was found and
212+
reassignment of the ot.ip_address was done.
213+
False if unable to find a device IP (error)
214+
215+
Raises:
216+
OnboardException("fail-general"):
217+
When a prefix was entered for an IP address
218+
OnboardException("fail-dns"):
219+
When a Name lookup via DNS fails to resolve an IP address
220+
"""
221+
try:
222+
# Assign checked_ip to None for error handling
223+
# If successful, this is an IP address and can pass
224+
checked_ip = netaddr.IPAddress(self.ot.ip_address)
225+
return True
226+
# Catch when someone has put in a prefix address, raise an exception
227+
except ValueError:
228+
raise OnboardException(
229+
reason="fail-general", message=f"ERROR appears a prefix was entered: {self.ot.ip_address}"
230+
)
231+
# An AddrFormatError exception means that there is not an IP address in the field, and should continue on
232+
except AddrFormatError:
233+
try:
234+
# Do a lookup of name to get the IP address to connect to
235+
checked_ip = socket.gethostbyname(self.ot.ip_address)
236+
self.ot.ip_address = checked_ip
237+
return True
238+
except socket.gaierror:
239+
# DNS Lookup has failed, Raise an exception for unable to complete DNS lookup
240+
raise OnboardException(
241+
reason="fail-dns", message=f"ERROR failed to complete DNS lookup: {self.ot.ip_address}"
242+
)
243+
201244
def get_required_info(
202245
self,
203246
default_mgmt_if=PLUGIN_SETTINGS["default_management_interface"],
@@ -215,6 +258,8 @@ def get_required_info(
215258
OnboardException('fail-general'):
216259
Any other unexpected device comms failure.
217260
"""
261+
# Check to see if the IP address entered was an IP address or a DNS entry, get the IP address
262+
self.check_ip()
218263
self.check_reachability()
219264
mgmt_ipaddr = self.ot.ip_address
220265

@@ -474,5 +519,6 @@ def ensure_device(self):
474519
self.ensure_device_type()
475520
self.ensure_device_role()
476521
self.ensure_device_instance()
477-
self.ensure_interface()
478-
self.ensure_primary_ip()
522+
if PLUGIN_SETTINGS["create_management_interface_if_missing"]:
523+
self.ensure_interface()
524+
self.ensure_primary_ip()

netbox_onboarding/tests/test_onboard.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
See the License for the specific language governing permissions and
1212
limitations under the License.
1313
"""
14+
from socket import gaierror
15+
from unittest import mock
1416
from django.test import TestCase
1517
from django.utils.text import slugify
1618

@@ -40,6 +42,16 @@ def setUp(self):
4042
self.onboarding_task3 = OnboardingTask.objects.create(
4143
ip_address="192.168.1.2", site=self.site1, role=self.device_role1, platform=self.platform1
4244
)
45+
self.onboarding_task4 = OnboardingTask.objects.create(
46+
ip_address="ntc123.local", site=self.site1, role=self.device_role1, platform=self.platform1
47+
)
48+
49+
self.onboarding_task5 = OnboardingTask.objects.create(
50+
ip_address="bad.local", site=self.site1, role=self.device_role1, platform=self.platform1
51+
)
52+
self.onboarding_task7 = OnboardingTask.objects.create(
53+
ip_address="192.0.2.1/32", site=self.site1, role=self.device_role1, platform=self.platform1
54+
)
4355

4456
self.ndk1 = NetdevKeeper(self.onboarding_task1)
4557
self.ndk1.hostname = "device1"
@@ -172,7 +184,7 @@ def test_ensure_interface_exist(self):
172184
self.assertEqual(nbk.interface, intf)
173185

174186
def test_ensure_primary_ip_not_exist(self):
175-
"""Verify ensure_primary_ip function when the Ip address do not already exist."""
187+
"""Verify ensure_primary_ip function when the IP address do not already exist."""
176188
nbk = NetboxKeeper(self.ndk2)
177189
nbk.device_type = self.device_type1
178190
nbk.netdev.ot = self.onboarding_task3
@@ -182,3 +194,38 @@ def test_ensure_primary_ip_not_exist(self):
182194
nbk.ensure_primary_ip()
183195
self.assertIsInstance(nbk.primary_ip, IPAddress)
184196
self.assertEqual(nbk.primary_ip.interface, nbk.interface)
197+
198+
@mock.patch("netbox_onboarding.onboard.socket.gethostbyname")
199+
def test_check_ip(self, mock_get_hostbyname):
200+
"""Check DNS to IP address."""
201+
# Look up response value
202+
mock_get_hostbyname.return_value = "192.0.2.1"
203+
204+
# Create a Device Keeper object of the device
205+
ndk4 = NetdevKeeper(self.onboarding_task4)
206+
207+
# Check that the IP address is returned
208+
self.assertTrue(ndk4.check_ip())
209+
210+
# Run the check to change the IP address
211+
self.assertEqual(ndk4.ot.ip_address, "192.0.2.1")
212+
213+
@mock.patch("netbox_onboarding.onboard.socket.gethostbyname")
214+
def test_failed_check_ip(self, mock_get_hostbyname):
215+
"""Check DNS to IP address failing."""
216+
# Look up a failed response
217+
mock_get_hostbyname.side_effect = gaierror(8)
218+
ndk5 = NetdevKeeper(self.onboarding_task5)
219+
ndk7 = NetdevKeeper(self.onboarding_task7)
220+
221+
# Check for bad.local raising an exception
222+
with self.assertRaises(OnboardException) as exc_info:
223+
ndk5.check_ip()
224+
self.assertEqual(exc_info.exception.message, "ERROR failed to complete DNS lookup: bad.local")
225+
self.assertEqual(exc_info.exception.reason, "fail-dns")
226+
227+
# Check for exception with prefix address entered
228+
with self.assertRaises(OnboardException) as exc_info:
229+
ndk7.check_ip()
230+
self.assertEqual(exc_info.exception.reason, "fail-prefix")
231+
self.assertEqual(exc_info.exception.message, "ERROR appears a prefix was entered: 192.0.2.1/32")

0 commit comments

Comments
 (0)