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

Shooks software version #309

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Regardless, the Onboarding App greatly simplifies the onboarding process by allo
| 802.1Q mode | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Lag Member | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Vrf Membership | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Software Version | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |

| VLANS | Cisco IOS | Cisco XE | Cisco NXOS | Cisco WLC | Juniper Junos | Arista EOS | F5 |
| ----------------------- | :----------------: | :--------------: | :--------------: | :--------------: | :--------------: | :--------------: | :-: |
Expand Down
1 change: 1 addition & 0 deletions changes/233.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added support syncing in software versions from devices to nautobot core models.
2 changes: 1 addition & 1 deletion docs/admin/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Here you will find detailed instructions on how to **install** and **configure**

## Prerequisites

- The app is compatible with Nautobot 2.0.3 and higher.
- The app is compatible with Nautobot 2.3.1 and higher.
- Databases supported: PostgreSQL, MySQL

!!! note
Expand Down
1 change: 1 addition & 0 deletions docs/user/app_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Expose two new SSoT based Nautobot jobs to perform the syncing of data.
- VRF Names
- Route Distinguishers (RD)
- Cabling
- Software Version

!!! info
For more information look at the provided jsonschema definitions for each of the jobs.
Expand Down
5 changes: 5 additions & 0 deletions nautobot_device_onboarding/command_mappers/arista_eos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,8 @@ sync_network_data:
jpath: '{admin_mode: switchports."{{ current_key }}".switchportInfo.mode, mode: switchports."{{ current_key }}".switchportInfo.mode, access_vlan: switchports."{{ current_key }}".switchportInfo.accessVlanId, trunking_vlans: switchports."{{ current_key }}".switchportInfo.trunkAllowedVlans, native_vlan: switchports."{{ current_key }}".switchportInfo.trunkingNativeVlanId}' # yamllint disable-line rule:quoted-strings
post_processor: "{{ obj | get_vlan_data(vlan_map, 'untagged') | tojson }}"
iterable_type: "dict"
software_version:
commands:
- command: "show version"
parser: "textfsm"
jpath: "[*].image"
5 changes: 5 additions & 0 deletions nautobot_device_onboarding/command_mappers/cisco_ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ sync_network_data:
parser: "textfsm"
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
post_processor: "{% set result = [] %}{% for cable in obj %}{% set _=result.append({'local_interface': cable['local_interface'], 'remote_interface': cable['remote_interface'], 'remote_device': cable['remote_device'] | remove_fqdn }) %}{% endfor %}{{ result | tojson }}"
software_version:
commands:
- command: "show version"
parser: "textfsm"
jpath: "[*].version"
5 changes: 5 additions & 0 deletions nautobot_device_onboarding/command_mappers/cisco_nxos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,8 @@ sync_network_data:
parser: "textfsm"
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
post_processor: "{% set result = [] %}{% for cable in obj %}{% set _=result.append({'local_interface': cable['local_interface'], 'remote_interface': cable['remote_interface'], 'remote_device': cable['remote_device'] | remove_fqdn }) %}{% endfor %}{{ result | tojson }}"
software_version:
commands:
- command: "show version"
parser: "textfsm"
jpath: "[*].os"
5 changes: 5 additions & 0 deletions nautobot_device_onboarding/command_mappers/cisco_xe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ sync_network_data:
parser: "textfsm"
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
post_processor: "{% set result = [] %}{% for cable in obj %}{% set _=result.append({'local_interface': cable['local_interface'], 'remote_interface': cable['remote_interface'], 'remote_device': cable['remote_device'] | remove_fqdn }) %}{% endfor %}{{ result | tojson }}"
software_version:
commands:
- command: "show version"
parser: "textfsm"
jpath: "[*].version"
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,8 @@ sync_network_data:
jpath: '"lldp-neighbors-information"[]."lldp-neighbor-information"[].{local_interface: "lldp-local-port-id"[0].data, remote_interface: "lldp-remote-port-id"[0].data, remote_device: "lldp-remote-system-name"[0].data}' # yamllint disable-line rule:quoted-strings
post_processor: "{% set result = [] %}{% for cable in obj %}{% set _=result.append({'local_interface': cable['local_interface'], 'remote_interface': cable['remote_interface'], 'remote_device': cable['remote_device'] | remove_fqdn }) %}{% endfor %}{{ result | tojson }}"
iterable_type: "dict"
software_version:
commands:
- command: "show system information | display json"
parser: "none"
jpath: '"system-information"[]."os-version"[].data' # yamllint disable-line rule:quoted-strings
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from diffsync.enum import DiffSyncModelFlags
from django.conf import settings
from django.core.exceptions import ValidationError
from nautobot.dcim.models import Interface
from nautobot.dcim.models import Device, Interface, SoftwareVersion
from nautobot.ipam.models import VLAN, VRF, IPAddress
from nautobot_ssot.contrib import NautobotAdapter
from netaddr import EUI, mac_unix_expanded
Expand Down Expand Up @@ -51,6 +51,8 @@ class SyncNetworkDataNautobotAdapter(FilteredNautobotAdapter):
lag_to_interface = sync_network_data_models.SyncNetworkDataLagToInterface
vrf_to_interface = sync_network_data_models.SyncNetworkDataVrfToInterface
cable = sync_network_data_models.SyncNetworkDataCable
software_version = sync_network_data_models.SyncNetworkSoftwareVersion
software_version_to_device = sync_network_data_models.SyncNetworkSoftwareVersionToDevice

primary_ips = None

Expand All @@ -65,6 +67,8 @@ class SyncNetworkDataNautobotAdapter(FilteredNautobotAdapter):
"lag_to_interface",
"vrf_to_interface",
"cable",
"software_version",
"software_version_to_device",
]

def _cache_primary_ips(self, device_queryset):
Expand Down Expand Up @@ -288,6 +292,32 @@ def load_cables(self):
except diffsync.exceptions.ObjectAlreadyExists:
continue

def load_software_versions(self):
"""Load Software Versions into the Diffsync store."""
for software_version in SoftwareVersion.objects.all():
network_software_version = self.software_version(
adapter=self,
version=software_version.version,
platform__name=software_version.platform.name,
)
try:
network_software_version.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST
self.add(network_software_version)
except diffsync.exceptions.ObjectAlreadyExists:
continue

def load_software_version_to_device(self):
"""Load Software Version to Device assignments into the Diffsync store."""
for device in self.job.devices_to_load:
network_software_version_to_device = self.software_version_to_device(
adapter=self,
name=device.name,
serial=device.serial,
software_version__version=device.software_version.version if device.software_version else "",
)
network_software_version_to_device.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST
self.add(network_software_version_to_device)

def load(self):
"""Generic implementation of the load function."""
if not hasattr(self, "top_level") or not self.top_level:
Expand Down Expand Up @@ -317,6 +347,12 @@ def load(self):
elif model_name == "cable":
if self.job.sync_cables:
self.load_cables()
elif model_name == "software_version":
if self.job.sync_software_version:
self.load_software_versions()
elif model_name == "software_version_to_device":
if self.job.sync_software_version:
self.load_software_version_to_device()
else:
diffsync_model = self._get_diffsync_class(model_name)
self._load_objects(diffsync_model)
Expand Down Expand Up @@ -392,6 +428,8 @@ def __init__(self, *args, job, sync=None, **kwargs):
lag_to_interface = sync_network_data_models.SyncNetworkDataLagToInterface
vrf_to_interface = sync_network_data_models.SyncNetworkDataVrfToInterface
cable = sync_network_data_models.SyncNetworkDataCable
software_version = sync_network_data_models.SyncNetworkSoftwareVersion
software_version_to_device = sync_network_data_models.SyncNetworkSoftwareVersionToDevice

top_level = [
"ip_address",
Expand All @@ -404,6 +442,8 @@ def __init__(self, *args, job, sync=None, **kwargs):
"lag_to_interface",
"vrf_to_interface",
"cable",
"software_version",
"software_version_to_device",
]

def _handle_failed_devices(self, device_data):
Expand Down Expand Up @@ -873,6 +913,46 @@ def load_cables(self): # pylint: disable=inconsistent-return-statements
model_type="cable",
)

def load_software_versions(self):
"""Load software versions into the Diffsync store."""
for ( # pylint: disable=too-many-nested-blocks
hostname,
device_data,
) in self.job.command_getter_result.items():
if self.job.debug:
self.job.logger.debug(f"Loading Software Versions from {hostname}")
if device_data["software_version"]:
device = Device.objects.get(serial=device_data["serial"])
try:
network_software_version = self.software_version(
adapter=self,
platform__name=device.platform.name,
version=device_data["software_version"],
)
self.add(network_software_version)
except diffsync.exceptions.ObjectAlreadyExists:
continue

def load_software_version_to_device(self):
"""Load software version to device assignments into the Diffsync store."""
for ( # pylint: disable=too-many-nested-blocks
hostname,
device_data,
) in self.job.command_getter_result.items():
if self.job.debug:
self.job.logger.debug(f"Loading Software Version to Device assignments from {hostname}")
if device_data["software_version"]:
try:
network_software_version_to_device = self.software_version_to_device(
adapter=self,
name=hostname,
serial=device_data["serial"],
software_version__version=device_data["software_version"],
)
self.add(network_software_version_to_device)
except diffsync.exceptions.ObjectAlreadyExists:
continue

def load(self):
"""Load network data."""
self.execute_command_getter()
Expand All @@ -891,3 +971,7 @@ def load(self):
self.load_vrf_to_interface()
if self.job.sync_cables:
self.load_cables()
if self.job.sync_software_version:
self.load_software_versions()
if self.job.sync_software_version:
self.load_software_version_to_device()
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Diffsync models."""

from typing import List, Optional
from uuid import UUID

try:
from typing import Annotated # Python>=3.9
Expand All @@ -11,7 +12,7 @@
from diffsync import exceptions as diffsync_exceptions
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError
from nautobot.dcim.choices import InterfaceTypeChoices
from nautobot.dcim.models import Cable, Device, Interface, Location
from nautobot.dcim.models import Cable, Device, Interface, Location, Platform, SoftwareVersion
from nautobot.extras.models import Status
from nautobot.ipam.models import VLAN, VRF, IPAddress, IPAddressToInterface
from nautobot_ssot.contrib import CustomFieldAnnotation, NautobotModel
Expand Down Expand Up @@ -590,3 +591,117 @@ class SyncNetworkDataCable(FilteredNautobotModel):
termination_b__name: str

status__name: str


class SyncNetworkSoftwareVersion(DiffSyncModel):
"""Shared data model representing a software version."""

_modelname = "software_version"
_model = SoftwareVersion
_identifiers = (
"version",
"platform__name",
)
_attributes = ()
_children = {}

version: str
platform__name: str

pk: Optional[UUID] = None

@classmethod
def create(cls, adapter, ids, attrs):
"""Create a new software version."""
try:
platform = Platform.objects.get(name=ids["platform__name"])
except ObjectDoesNotExist:
adapter.job.logger.error(
f"Failed to create software version {ids['version']}. An platform with name: "
f"{ids['platform__name']} was not found."
)
raise diffsync_exceptions.ObjectNotCreated
try:
software_version = SoftwareVersion(
version=ids["version"],
platform=platform,
status=Status.objects.get(name="Active"),
)
software_version.validated_save()
except ValidationError as err:
adapter.job.logger.error(f"Software version {software_version} failed to create, {err}")
raise diffsync_exceptions.ObjectNotCreated

return super().create(adapter, ids, attrs)

def delete(self):
"""Prevent software version deletion."""
self.adapter.job.logger.error(f"{self} will not be deleted.")
return None


class SyncNetworkSoftwareVersionToDevice(DiffSyncModel):
"""Shared data model representing a software version to device."""

_model = Device
_modelname = "software_version_to_device"
_identifiers = (
"name",
"serial",
)
_attributes = ("software_version__version",)

name: str
serial: str
software_version__version: str

def _get_and_assign_sofware_version(self, adapter, attrs):
"""Assign a software version to a device."""
try:
device = Device.objects.get(**self.get_identifiers())
except ObjectDoesNotExist:
adapter.job.logger.error(
f"Failed to assign software version to {self.name}. An device with name: " f"{self.name} was not found."
)
raise diffsync_exceptions.ObjectNotCreated
try:
software_version = SoftwareVersion.objects.get(
version=attrs["software_version__version"], platform=device.platform
)
device.software_version = software_version
except ObjectDoesNotExist:
adapter.job.logger.error(
f"Failed to assign software version to {self.name}. An software version with name: "
f"{self.name} was not found."
)
raise diffsync_exceptions.ObjectNotUpdated
try:
device.validated_save()
except ValidationError as err:
adapter.job.logger.error(f"Software version {software_version} failed to assign, {err}")
raise diffsync_exceptions.ObjectNotUpdated

def update(self, attrs):
"""Update an existing SoftwareVersionToDevice object."""
if attrs.get("software_version__version"):
try:
self._get_and_assign_sofware_version(self.adapter, attrs)
except ObjectDoesNotExist as err:
self.adapter.job.logger.error(f"{self} failed to update, {err}")
raise diffsync_exceptions.ObjectNotUpdated

return super().update(attrs)

@classmethod
def create(cls, adapter, ids, attrs):
"""
Do not create new devices.

Network devices need to exist in Nautobot prior to syncing data and
need to be included in the queryset generated based on job form inputs.
"""
return None

def delete(self):
"""Prevent device deletion."""
return None
Loading
Loading