Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FQDN support for device onboarding #251

Merged
merged 8 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/238.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a fix for mtu as an integer rather than a string.
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
Expand Up @@ -3,6 +3,7 @@
from collections import defaultdict
from typing import DefaultDict, Dict, FrozenSet, Hashable, Tuple, Type

import socket
import diffsync
import netaddr
from django.contrib.contenttypes.models import ContentType
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 @@ -54,15 +57,19 @@ def get_from_orm_cache(self, parameters: Dict, model_class: Type[Model]):
return cached_object
# As we are using `get` here, this will error if there is not exactly one object that corresponds to the
# parameter set. We intentionally pass these errors through.
self._cache[model_cache_key][parameter_set] = model_class.objects.get(**dict(parameter_set))
self._cache[model_cache_key][parameter_set] = model_class.objects.get(
**dict(parameter_set)
)
return self._cache[model_cache_key][parameter_set]

def load_manufacturers(self):
"""Load manufacturer data from Nautobot."""
for manufacturer in Manufacturer.objects.all():
if self.job.debug:
self.job.logger.debug("Loading Manufacturer data from Nautobot...")
onboarding_manufacturer = self.manufacturer(adapter=self, pk=manufacturer.pk, name=manufacturer.name)
onboarding_manufacturer = self.manufacturer(
adapter=self, pk=manufacturer.pk, name=manufacturer.name
)
self.add(onboarding_manufacturer)
if self.job.debug:
self.job.logger.debug(f"Manufacturer: {manufacturer.name} loaded.")
Expand All @@ -76,8 +83,12 @@ 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 All @@ -104,7 +115,9 @@ def load_devices(self):
if self.job.debug:
self.job.logger.debug("Loading Device data from Nautobot...")

for device in Device.objects.filter(primary_ip4__host__in=self.job.ip_addresses):
for device in Device.objects.filter(
primary_ip4__host__in=self.job.ip_addresses
):
interface_list = []
# Only interfaces with the device's primary ip should be considered for diff calculations
# Ultimately, only the first matching interface is used but this list could support multiple
Expand All @@ -125,12 +138,18 @@ 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 +186,19 @@ 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 All @@ -188,7 +214,9 @@ def _handle_failed_devices(self, device_data):
self.failed_ip_addresses = []
for ip_address in device_data:
if not device_data[ip_address]:
self.job.logger.error(f"{ip_address}: Connection or data error, this device will not be synced.")
self.job.logger.error(
f"{ip_address}: Connection or data error, this device will not be synced."
)
self.failed_ip_addresses.append(ip_address)
for ip_address in self.failed_ip_addresses:
del device_data[ip_address]
Expand All @@ -203,16 +231,22 @@ 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}")
data_type_check = diffsync_utils.check_data_type(result)
if self.job.debug:
self.job.logger.debug(f"CommandGetter data type check resut: {data_type_check}")
self.job.logger.debug(
f"CommandGetter data type check resut: {data_type_check}"
)
if data_type_check:
self._handle_failed_devices(device_data=result)
else:
Expand Down Expand Up @@ -297,8 +331,16 @@ 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"]
if platform: # platform is only returned with device data if not provided on the job form/csv
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
susanhooks marked this conversation as resolved.
Show resolved Hide resolved
required_fields_from_device.append("platform")
for field in required_fields_from_device:
data = device_data[ip_address]
Expand All @@ -311,7 +353,9 @@ def load_devices(self):
for ip_address in self.device_data:
if self.job.debug:
self.job.logger.debug(f"loading device data for {ip_address}")
platform = None # If an excption is caught below, the platform must still be set.
platform = (
None # If an excption is caught below, the platform must still be set.
)
susanhooks marked this conversation as resolved.
Show resolved Hide resolved
onboarding_device = None
try:
location = diffsync_utils.retrieve_submitted_value(
Expand All @@ -321,7 +365,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 +384,11 @@ 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 Expand Up @@ -372,7 +422,9 @@ def load_devices(self):
try:
self.add(onboarding_device)
if self.job.debug:
self.job.logger.debug(f"Device: {self.device_data[ip_address]['hostname']} loaded.")
self.job.logger.debug(
f"Device: {self.device_data[ip_address]['hostname']} loaded."
)
except diffsync.ObjectAlreadyExists:
self.job.logger.error(
f"Device: {self.device_data[ip_address]['hostname']} has already been loaded! "
Expand All @@ -383,7 +435,9 @@ def load_devices(self):
else:
self._add_ip_address_to_failed_list(ip_address=ip_address)
if self.job.debug:
self.job.logger.debug(f"{ip_address} was added to the failed ip_address list")
self.job.logger.debug(
f"{ip_address} was added to the failed ip_address list"
)

def load(self):
"""Load network data."""
Expand Down
Loading