Skip to content

Commit

Permalink
Assign default custom fields values to new objects
Browse files Browse the repository at this point in the history
Validated save for created objects
Fix for missing status in IP Address
Fix device role colour
  • Loading branch information
mzbroch committed May 31, 2021
1 parent 0029535 commit 9232ee4
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 4 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -68,7 +68,8 @@ The plugin behavior can be controlled with the following list of settings
- `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.
- `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.
- `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 Nautobot, while the device will still get added.
- `default_device_status` string (default "Active"), status assigned to a new device by default (must be lowercase).
- `default_device_status` string (default "Active"), status assigned to a new device by default.
- `default_ip_status` string (default "Active"), status assigned to a new device management IP.
- `default_device_role` string (default "network")
- `default_device_role_color` string (default FF0000), color assigned to the device role if it needs to be created.
- `default_management_interface` string (default "PLACEHOLDER"), name of the management interface that will be created, if one can't be identified on the device.
Expand Down
3 changes: 2 additions & 1 deletion nautobot_device_onboarding/__init__.py
Expand Up @@ -35,10 +35,11 @@ class OnboardingConfig(PluginConfig):
"create_device_type_if_missing": True,
"create_device_role_if_missing": True,
"default_device_role": "network",
"default_device_role_color": "FF0000",
"default_device_role_color": "ff0000",
"default_management_interface": "PLACEHOLDER",
"default_management_prefix_length": 0,
"default_device_status": "Active",
"default_ip_status": "Active",
"create_management_interface_if_missing": True,
"skip_device_type_on_update": False,
"skip_manufacturer_on_update": False,
Expand Down
41 changes: 40 additions & 1 deletion nautobot_device_onboarding/nautobot_keeper.py
Expand Up @@ -17,12 +17,14 @@

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.text import slugify
from nautobot.dcim.choices import InterfaceTypeChoices
from nautobot.dcim.models import Manufacturer, Device, Interface, DeviceType, DeviceRole
from nautobot.dcim.models import Platform
from nautobot.dcim.models import Site
from nautobot.extras.models import Status
from nautobot.extras.models.customfields import CustomField
from nautobot.ipam.models import IPAddress

from .constants import NETMIKO_TO_NAPALM_STATIC
Expand All @@ -33,6 +35,21 @@
PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_device_onboarding"]


def ensure_default_cf(obj, model):
"""Update objects's default custom fields."""
for cf in CustomField.objects.get_for_model(model):
if (cf.default is not None) and (cf.name not in obj.cf):
obj.cf[cf.name] = cf.default

try:
obj.validated_save()
except ValidationError as err:
raise OnboardException(
reason="fail-general",
message=f"ERROR: {obj} validation error: {err.messages}",
)


def object_match(obj, search_array):
"""Used to search models for multiple criteria.
Expand Down Expand Up @@ -192,6 +209,7 @@ def ensure_device_manufacturer(
except Manufacturer.DoesNotExist:
if create_manufacturer:
self.nb_manufacturer = Manufacturer.objects.create(name=self.netdev_vendor, slug=nb_manufacturer_slug)
ensure_default_cf(obj=self.nb_manufacturer, model=Manufacturer)
else:
raise OnboardException(
reason="fail-config", message=f"ERROR manufacturer not found: {self.netdev_vendor}"
Expand Down Expand Up @@ -264,6 +282,7 @@ def ensure_device_type(
model=nb_device_type_slug.upper(),
manufacturer=self.nb_manufacturer,
)
ensure_default_cf(obj=self.nb_device_type, model=DeviceType)
else:
raise OnboardException(
reason="fail-config", message=f"ERROR device type not found: {self.netdev_model}"
Expand Down Expand Up @@ -292,6 +311,7 @@ def ensure_device_role(
color=self.netdev_nb_role_color,
vm_role=False,
)
ensure_default_cf(obj=self.nb_device_role, model=DeviceRole)
else:
raise OnboardException(
reason="fail-config", message=f"ERROR device role not found: {self.netdev_nb_role_slug}"
Expand Down Expand Up @@ -344,6 +364,7 @@ def ensure_device_platform(self, create_platform_if_missing=PLUGIN_SETTINGS["cre
slug=self.netdev_nb_platform_slug,
napalm_driver=netmiko_to_napalm[self.netdev_netmiko_device_type],
)
ensure_default_cf(obj=self.nb_platform, model=Platform)
else:
raise OnboardException(
reason="fail-general",
Expand Down Expand Up @@ -407,6 +428,7 @@ def ensure_device_instance(self, default_status=PLUGIN_SETTINGS["default_device_

try:
self.device, created = Device.objects.update_or_create(**lookup_args)
ensure_default_cf(obj=self.device, model=Device)

if created:
logger.info("CREATED device: %s", self.netdev_hostname)
Expand All @@ -425,14 +447,31 @@ def ensure_interface(self):
self.nb_mgmt_ifname, _ = Interface.objects.get_or_create(
name=self.netdev_mgmt_ifname, device=self.device, defaults=dict(type=InterfaceTypeChoices.TYPE_OTHER)
)
ensure_default_cf(obj=self.nb_mgmt_ifname, model=Interface)

def ensure_primary_ip(self):
"""Ensure mgmt_ipaddr exists in IPAM, has the device interface, and is assigned as the primary IP address."""
# see if the primary IP address exists in IPAM
if self.netdev_mgmt_ip_address and self.netdev_mgmt_pflen:
ct = ContentType.objects.get_for_model(IPAddress) # pylint: disable=invalid-name
default_status_name = PLUGIN_SETTINGS["default_ip_status"]
try:
ip_status = Status.objects.get(content_types__in=[ct], name=default_status_name)
except Status.DoesNotExist:
raise OnboardException(
reason="fail-general",
message=f"ERROR could not find existing IP Address status: {default_status_name}",
)
except Status.MultipleObjectsReturned:
raise OnboardException(
reason="fail-general",
message=f"ERROR multiple IP Address status using same name: {default_status_name}",
)

self.nb_primary_ip, created = IPAddress.objects.get_or_create(
address=f"{self.netdev_mgmt_ip_address}/{self.netdev_mgmt_pflen}"
address=f"{self.netdev_mgmt_ip_address}/{self.netdev_mgmt_pflen}", defaults={"status": ip_status}
)
ensure_default_cf(obj=self.nb_primary_ip, model=IPAddress)

if created or self.nb_primary_ip not in self.nb_mgmt_ifname.ip_addresses.all():
logger.info("ASSIGN: IP address %s to %s", self.nb_primary_ip.address, self.nb_mgmt_ifname.name)
Expand Down
83 changes: 82 additions & 1 deletion nautobot_device_onboarding/tests/test_nautobot_keeper.py
Expand Up @@ -18,7 +18,8 @@
from nautobot.dcim.choices import InterfaceTypeChoices
from nautobot.dcim.models import Site, Manufacturer, DeviceType, DeviceRole, Device, Interface, Platform
from nautobot.ipam.models import IPAddress
from nautobot.extras.models import Status
from nautobot.extras.choices import CustomFieldTypeChoices
from nautobot.extras.models import CustomField, Status

from nautobot_device_onboarding.exceptions import OnboardException
from nautobot_device_onboarding.nautobot_keeper import NautobotKeeper
Expand All @@ -32,6 +33,57 @@ class NautobotKeeperTestCase(TestCase):
def setUp(self):
"""Create a superuser and token for API calls."""
self.site1 = Site.objects.create(name="USWEST", slug="uswest")
DATA = (
{
"field_type": CustomFieldTypeChoices.TYPE_TEXT,
"field_name": "cf_manufacturer",
"default_value": "Foobar!",
"model": Manufacturer,
},
{
"field_type": CustomFieldTypeChoices.TYPE_INTEGER,
"field_name": "cf_devicetype",
"default_value": 5,
"model": DeviceType,
},
{
"field_type": CustomFieldTypeChoices.TYPE_INTEGER,
"field_name": "cf_devicerole",
"default_value": 10,
"model": DeviceRole,
},
{
"field_type": CustomFieldTypeChoices.TYPE_BOOLEAN,
"field_name": "cf_platform",
"default_value": True,
"model": Platform,
},
{
"field_type": CustomFieldTypeChoices.TYPE_BOOLEAN,
"field_name": "cf_device",
"default_value": False,
"model": Device,
},
{
"field_type": CustomFieldTypeChoices.TYPE_DATE,
"field_name": "cf_interface",
"default_value": "2016-06-23",
"model": Interface,
},
{
"field_type": CustomFieldTypeChoices.TYPE_URL,
"field_name": "cf_ipaddress",
"default_value": "http://example.com/",
"model": IPAddress,
},
)

for data in DATA:
# Create a custom field
cf = CustomField.objects.create(
type=data["field_type"], name=data["field_name"], default=data["default_value"], required=False
)
cf.content_types.set([ContentType.objects.get_for_model(data["model"])])

def test_ensure_device_manufacturer_strict_missing(self):
"""Verify ensure_device_manufacturer function when Manufacturer object is not present."""
Expand Down Expand Up @@ -473,3 +525,32 @@ def test_platform_map(self):
Platform.objects.get(name=PLUGIN_SETTINGS["platform_map"]["cisco_ios"]).name,
slugify(PLUGIN_SETTINGS["platform_map"]["cisco_ios"]),
)

def test_ensure_custom_fields(self):
"""Verify objects inherit default custom fields."""
onboarding_kwargs = {
"netdev_hostname": "sw1",
"netdev_nb_role_slug": "switch",
"netdev_vendor": "Cisco",
"netdev_model": "c2960",
"netdev_nb_site_slug": self.site1.slug,
"netdev_netmiko_device_type": "cisco_ios",
"netdev_serial_number": "123456",
"netdev_mgmt_ip_address": "192.0.2.15",
"netdev_mgmt_ifname": "Management0",
"netdev_mgmt_pflen": 24,
"netdev_nb_role_color": "ff0000",
}

nbk = NautobotKeeper(**onboarding_kwargs)
nbk.ensure_device()

device = Device.objects.get(name="sw1")

self.assertEqual(device.cf["cf_device"], False)
self.assertEqual(device.platform.cf["cf_platform"], True)
self.assertEqual(device.device_type.cf["cf_devicetype"], 5)
self.assertEqual(device.device_role.cf["cf_devicerole"], 10)
self.assertEqual(device.device_type.manufacturer.cf["cf_manufacturer"], "Foobar!")
self.assertEqual(device.interfaces.get(name="Management0").cf["cf_interface"], "2016-06-23")
self.assertEqual(device.primary_ip.cf["cf_ipaddress"], "http://example.com/")

0 comments on commit 9232ee4

Please sign in to comment.