Skip to content

Commit

Permalink
Add FQDN support for device onboarding (#251)
Browse files Browse the repository at this point in the history
* update to fix mtu int error
* added fqdn support, formatting
---------

Co-authored-by: Susan Hooks <susan.hooks@chevron.com>
  • Loading branch information
susanhooks and Susan Hooks authored Oct 18, 2024
1 parent 58fcb32 commit 2753924
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 34 deletions.
1 change: 1 addition & 0 deletions changes/241.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added FQDN support to the sync network device job.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""DiffSync adapters."""

import socket
from collections import defaultdict
from typing import DefaultDict, Dict, FrozenSet, Hashable, Tuple, Type

Expand All @@ -11,7 +12,9 @@
from nautobot.dcim.models import Device, DeviceType, Manufacturer, Platform

from nautobot_device_onboarding.diffsync.models import sync_devices_models
from nautobot_device_onboarding.nornir_plays.command_getter import sync_devices_command_getter
from nautobot_device_onboarding.nornir_plays.command_getter import (
sync_devices_command_getter,
)
from nautobot_device_onboarding.utils import diffsync_utils

ParameterSet = FrozenSet[Tuple[str, Hashable]]
Expand Down Expand Up @@ -76,8 +79,8 @@ def load_platforms(self):
adapter=self,
pk=platform.pk,
name=platform.name,
network_driver=platform.network_driver if platform.network_driver else "",
manufacturer__name=platform.manufacturer.name if platform.manufacturer else None,
network_driver=(platform.network_driver if platform.network_driver else ""),
manufacturer__name=(platform.manufacturer.name if platform.manufacturer else None),
)
self.add(onboarding_platform)
if self.job.debug:
Expand Down Expand Up @@ -125,12 +128,12 @@ def load_devices(self):
name=device.name,
platform__name=device.platform.name if device.platform else "",
primary_ip4__host=device.primary_ip4.host if device.primary_ip4 else "",
primary_ip4__status__name=device.primary_ip4.status.name if device.primary_ip4 else "",
primary_ip4__status__name=(device.primary_ip4.status.name if device.primary_ip4 else ""),
role__name=device.role.name,
status__name=device.status.name,
secrets_group__name=device.secrets_group.name if device.secrets_group else "",
secrets_group__name=(device.secrets_group.name if device.secrets_group else ""),
interfaces=interfaces,
mask_length=device.primary_ip4.mask_length if device.primary_ip4 else None,
mask_length=(device.primary_ip4.mask_length if device.primary_ip4 else None),
serial=device.serial,
)
self.add(onboarding_device)
Expand Down Expand Up @@ -167,12 +170,17 @@ def _validate_ip_addresses(self, ip_addresses):
"""Validate the format of each IP Address in a list of IP Addresses."""
# Validate IP Addresses
validation_successful = True
for ip_address in ip_addresses:
for i, ip_address in enumerate(ip_addresses):
try:
netaddr.IPAddress(ip_address)
except netaddr.AddrFormatError:
self.job.logger.error(f"[{ip_address}] is not a valid IP Address ")
validation_successful = False
try:
resolved_ip = socket.gethostbyname(ip_address)
self.job.logger.info(f"[{ip_address}] resolved to [{resolved_ip}]")
ip_addresses[i] = resolved_ip
except socket.gaierror:
self.job.logger.error(f"[{ip_address}] is not a valid IP Address or name.")
validation_successful = False
if validation_successful:
return True
raise netaddr.AddrConversionError
Expand Down Expand Up @@ -203,10 +211,14 @@ def execute_command_getter(self):
f"The selected platform, {self.job.platform} "
"does not have a network driver, please update the Platform."
)
raise Exception("Platform.network_driver missing") # pylint: disable=broad-exception-raised
raise Exception( # pylint: disable=broad-exception-raised
"Platform.network_driver missing"
)

result = sync_devices_command_getter(
self.job.job_result, self.job.logger.getEffectiveLevel(), self.job.job_result.task_kwargs
self.job.job_result,
self.job.logger.getEffectiveLevel(),
self.job.job_result.task_kwargs,
)
if self.job.debug:
self.job.logger.debug(f"Command Getter Result: {result}")
Expand Down Expand Up @@ -297,7 +309,13 @@ def load_device_types(self):
def _fields_missing_data(self, device_data, ip_address, platform):
"""Verify that all of the fields returned from a device actually contain data."""
fields_missing_data = []
required_fields_from_device = ["device_type", "hostname", "mgmt_interface", "mask_length", "serial"]
required_fields_from_device = [
"device_type",
"hostname",
"mgmt_interface",
"mask_length",
"serial",
]
if platform: # platform is only returned with device data if not provided on the job form/csv
required_fields_from_device.append("platform")
for field in required_fields_from_device:
Expand All @@ -321,7 +339,9 @@ def load_devices(self):
job=self.job, ip_address=ip_address, query_string="platform"
)
primary_ip4__status = diffsync_utils.retrieve_submitted_value(
job=self.job, ip_address=ip_address, query_string="ip_address_status"
job=self.job,
ip_address=ip_address,
query_string="ip_address_status",
)
device_role = diffsync_utils.retrieve_submitted_value(
job=self.job, ip_address=ip_address, query_string="device_role"
Expand All @@ -338,7 +358,7 @@ def load_devices(self):
device_type__model=self.device_data[ip_address]["device_type"],
location__name=location.name,
name=self.device_data[ip_address]["hostname"],
platform__name=platform.name if platform else self.device_data[ip_address]["platform"],
platform__name=(platform.name if platform else self.device_data[ip_address]["platform"]),
primary_ip4__host=ip_address,
primary_ip4__status__name=primary_ip4__status.name,
role__name=device_role.name,
Expand Down
91 changes: 71 additions & 20 deletions nautobot_device_onboarding/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,30 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from nautobot.apps.jobs import BooleanVar, ChoiceVar, FileVar, IntegerVar, Job, MultiObjectVar, ObjectVar, StringVar
from nautobot.apps.jobs import (
BooleanVar,
ChoiceVar,
FileVar,
IntegerVar,
Job,
MultiObjectVar,
ObjectVar,
StringVar,
)
from nautobot.core.celery import register_jobs
from nautobot.dcim.models import Device, DeviceType, Location, Platform
from nautobot.extras.choices import CustomFieldTypeChoices, SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices
from nautobot.extras.models import CustomField, Role, SecretsGroup, SecretsGroupAssociation, Status
from nautobot.extras.choices import (
CustomFieldTypeChoices,
SecretsGroupAccessTypeChoices,
SecretsGroupSecretTypeChoices,
)
from nautobot.extras.models import (
CustomField,
Role,
SecretsGroup,
SecretsGroupAssociation,
Status,
)
from nautobot.ipam.models import Namespace
from nautobot_plugin_nornir.constants import NORNIR_SETTINGS
from nautobot_ssot.jobs.base import DataSource
Expand All @@ -32,7 +51,10 @@
)
from nautobot_device_onboarding.exceptions import OnboardException
from nautobot_device_onboarding.netdev_keeper import NetdevKeeper
from nautobot_device_onboarding.nornir_plays.command_getter import _parse_credentials, netmiko_send_commands
from nautobot_device_onboarding.nornir_plays.command_getter import (
_parse_credentials,
netmiko_send_commands,
)
from nautobot_device_onboarding.nornir_plays.empty_inventory import EmptyInventory
from nautobot_device_onboarding.nornir_plays.inventory_creator import _set_inventory
from nautobot_device_onboarding.nornir_plays.logger import NornirLogger
Expand Down Expand Up @@ -62,7 +84,9 @@ class OnboardingTask(Job): # pylint: disable=too-many-instance-attributes
port = IntegerVar(default=22)
timeout = IntegerVar(default=30)
credentials = ObjectVar(
model=SecretsGroup, required=False, description="SecretsGroup for Device connection credentials."
model=SecretsGroup,
required=False,
description="SecretsGroup for Device connection credentials.",
)
platform = ObjectVar(
model=Platform,
Expand Down Expand Up @@ -128,7 +152,9 @@ def run(self, *args, **data):
self._onboard(address=address)
except OnboardException as err:
self.logger.exception(
"The following exception occurred when attempting to onboard %s: %s", address, str(err)
"The following exception occurred when attempting to onboard %s: %s",
address,
str(err),
)
if not data["continue_on_failure"]:
raise OnboardException(
Expand All @@ -146,7 +172,7 @@ def _onboard(self, address):
username=self.username,
password=self.password,
secret=self.secret,
napalm_driver=self.platform.napalm_driver if self.platform and self.platform.napalm_driver else None,
napalm_driver=(self.platform.napalm_driver if self.platform and self.platform.napalm_driver else None),
optional_args=(
self.platform.napalm_args if self.platform and self.platform.napalm_args else settings.NAPALM_ARGS
),
Expand All @@ -159,10 +185,10 @@ def _onboard(self, address):
"netdev_mgmt_ip_address": address,
"netdev_nb_location_name": self.location.name,
"netdev_nb_device_type_name": self.device_type,
"netdev_nb_role_name": self.role.name if self.role else PLUGIN_SETTINGS["default_device_role"],
"netdev_nb_role_name": (self.role.name if self.role else PLUGIN_SETTINGS["default_device_role"]),
"netdev_nb_role_color": PLUGIN_SETTINGS["default_device_role_color"],
"netdev_nb_platform_name": self.platform.name if self.platform else None,
"netdev_nb_credentials": self.credentials if PLUGIN_SETTINGS["assign_secrets_group"] else None,
"netdev_nb_credentials": (self.credentials if PLUGIN_SETTINGS["assign_secrets_group"] else None),
# Kwargs discovered on the Onboarded Device:
"netdev_hostname": netdev_dict["netdev_hostname"],
"netdev_vendor": netdev_dict["netdev_vendor"],
Expand All @@ -175,10 +201,16 @@ def _onboard(self, address):
"driver_addon_result": netdev_dict["driver_addon_result"],
}
onboarding_cls = netdev_dict["onboarding_class"]()
onboarding_cls.credentials = {"username": self.username, "password": self.password, "secret": self.secret}
onboarding_cls.credentials = {
"username": self.username,
"password": self.password,
"secret": self.secret,
}
onboarding_cls.run(onboarding_kwargs=onboarding_kwargs)
self.logger.info(
"Successfully onboarded %s with a management IP of %s", netdev_dict["netdev_hostname"], address
"Successfully onboarded %s with a management IP of %s",
netdev_dict["netdev_hostname"],
address,
)

def _parse_credentials(self, credentials):
Expand Down Expand Up @@ -236,7 +268,9 @@ class Meta:
description="Enable for more verbose logging.",
)
csv_file = FileVar(
label="CSV File", required=False, description="If a file is provided all the options below will be ignored."
label="CSV File",
required=False,
description="If a file is provided all the options below will be ignored.",
)
location = ObjectVar(
model=Location,
Expand All @@ -247,7 +281,7 @@ class Meta:
namespace = ObjectVar(model=Namespace, required=False, description="Namespace ip addresses belong to.")
ip_addresses = StringVar(
required=False,
description="IP address of the device to sync, specify in a comma separated list for multiple devices.",
description="IP address or FQDN of the device to sync, specify in a comma separated list for multiple devices.",
label="IPv4 addresses",
)
port = IntegerVar(required=False, default=22)
Expand Down Expand Up @@ -288,7 +322,9 @@ class Meta:
description="Status to be applied to all new synced IP addresses. This value does not update with additional syncs.",
)
secrets_group = ObjectVar(
model=SecretsGroup, required=False, description="SecretsGroup for device connection credentials."
model=SecretsGroup,
required=False,
description="SecretsGroup for device connection credentials.",
)
platform = ObjectVar(
model=Platform,
Expand Down Expand Up @@ -333,7 +369,8 @@ def _process_csv_data(self, csv_file):
query = f"location_name: {row.get('location_name')}, location_parent_name: {row.get('location_parent_name')}"
if row.get("location_parent_name"):
location = Location.objects.get(
name=row["location_name"].strip(), parent__name=row["location_parent_name"].strip()
name=row["location_name"].strip(),
parent__name=row["location_parent_name"].strip(),
)
else:
query = query = f"location_name: {row.get('location_name')}"
Expand Down Expand Up @@ -452,7 +489,10 @@ def run(
for ip_address in self.processed_csv_data:
self.ip_addresses.append(ip_address)
# prepare the task_kwargs needed by the CommandGetterDO job
self.job_result.task_kwargs = {"debug": debug, "csv_file": self.task_kwargs_csv_data}
self.job_result.task_kwargs = {
"debug": debug,
"csv_file": self.task_kwargs_csv_data,
}
else:
raise ValidationError(message="CSV check failed. No devices will be synced.")

Expand Down Expand Up @@ -536,7 +576,9 @@ class Meta:
sync_vrfs = BooleanVar(default=False, description="Sync VRFs and interface VRF assignments.")
sync_cables = BooleanVar(default=False, description="Sync cables between interfaces via a LLDP or CDP.")
namespace = ObjectVar(
model=Namespace, required=True, description="The namespace for all IP addresses created or updated in the sync."
model=Namespace,
required=True,
description="The namespace for all IP addresses created or updated in the sync.",
)
interface_status = ObjectVar(
model=Status,
Expand Down Expand Up @@ -632,7 +674,9 @@ def run(
if self.debug:
self.logger.debug("Checking for last_network_data_sync custom field")
try:
cf = CustomField.objects.get(key="last_network_data_sync") # pylint:disable=invalid-name
cf = CustomField.objects.get( # pylint:disable=invalid-name
key="last_network_data_sync"
)
except ObjectDoesNotExist:
cf, _ = CustomField.objects.get_or_create( # pylint:disable=invalid-name
label="Last Network Data Sync",
Expand Down Expand Up @@ -716,7 +760,9 @@ def run(self, *args, **kwargs): # pragma: no cover
ip_addresses = kwargs["ip_addresses"].replace(" ", "").split(",")
port = kwargs["port"]
platform = kwargs["platform"]
username, password, secret = _parse_credentials(kwargs["secrets_group"]) # pylint:disable=unused-variable
username, password, secret = ( # pylint:disable=unused-variable
_parse_credentials(kwargs["secrets_group"])
)

# Initiate Nornir instance with empty inventory
compiled_results = {}
Expand Down Expand Up @@ -771,5 +817,10 @@ def run(self, *args, **kwargs): # pragma: no cover
return f"Successfully ran the following commands: {', '.join(list(compiled_results.keys()))}"


jobs = [OnboardingTask, SSOTSyncDevices, SSOTSyncNetworkData, DeviceOnboardingTroubleshootingJob]
jobs = [
OnboardingTask,
SSOTSyncDevices,
SSOTSyncNetworkData,
DeviceOnboardingTroubleshootingJob,
]
register_jobs(*jobs)

0 comments on commit 2753924

Please sign in to comment.