Skip to content

Commit 439c761

Browse files
susanhooksSusan Hooksscetron
authored
Software version (#309)
Added ability to sync software version from device. --------- Co-authored-by: Susan Hooks <susan.hooks@chevron.com> Co-authored-by: scetron <stephen.corry@networktocode.com>
1 parent 9bdb769 commit 439c761

File tree

29 files changed

+4538
-23
lines changed

29 files changed

+4538
-23
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
fail-fast: true
9393
matrix:
9494
python-version: ["3.11"]
95-
nautobot-version: ["2.2.3"]
95+
nautobot-version: ["2.3.1"]
9696
env:
9797
INVOKE_NAUTOBOT_DEVICE_ONBOARDING_PYTHON_VER: "${{ matrix.python-version }}"
9898
INVOKE_NAUTOBOT_DEVICE_ONBOARDING_NAUTOBOT_VER: "${{ matrix.nautobot-version }}"
@@ -145,7 +145,7 @@ jobs:
145145
include:
146146
- python-version: "3.11"
147147
db-backend: "postgresql"
148-
nautobot-version: "2.2.3"
148+
nautobot-version: "2.3.1"
149149
- python-version: "3.12"
150150
db-backend: "mysql"
151151
nautobot-version: "stable"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ Regardless, the Onboarding App greatly simplifies the onboarding process by allo
4444
| Link Status ||||||||
4545
| 802.1Q mode ||||||||
4646
| Lag Member ||||||||
47-
| VRF Membership ||||||||
47+
| Vrf Membership ||||||||
48+
| Software Version ||||||||
4849

4950
| VLANS | Cisco IOS | Cisco XE | Cisco NXOS | Cisco WLC | Juniper Junos | Arista EOS | F5 |
5051
| ----------------------- | :----------------: | :--------------: | :--------------: | :--------------: | :--------------: | :--------------: | :-: |

changes/233.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added support syncing in software versions from devices to nautobot core models.

docs/admin/install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Here you will find detailed instructions on how to **install** and **configure**
44

55
## Prerequisites
66

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

1010
!!! note

docs/user/app_overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Expose two new SSoT based Nautobot jobs to perform the syncing of data.
4747
- VRFs
4848
- VRF Names
4949
- Route Distinguishers (RD)
50+
- Cabling
51+
- Software Version
5052
- Cabling (**Note** Cables attached to Circuits will be skipped)
5153

5254
!!! info

nautobot_device_onboarding/command_mappers/arista_eos.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,8 @@ sync_network_data:
116116
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
117117
post_processor: "{{ obj | get_vlan_data(vlan_map, 'untagged') | tojson }}"
118118
iterable_type: "dict"
119+
software_version:
120+
commands:
121+
- command: "show version"
122+
parser: "textfsm"
123+
jpath: "[*].image"

nautobot_device_onboarding/command_mappers/cisco_ios.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,8 @@ sync_network_data:
129129
parser: "textfsm"
130130
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
131131
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 }}"
132+
software_version:
133+
commands:
134+
- command: "show version"
135+
parser: "textfsm"
136+
jpath: "[*].version"

nautobot_device_onboarding/command_mappers/cisco_nxos.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,8 @@ sync_network_data:
122122
parser: "textfsm"
123123
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
124124
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 }}"
125+
software_version:
126+
commands:
127+
- command: "show version"
128+
parser: "textfsm"
129+
jpath: "[*].os"

nautobot_device_onboarding/command_mappers/cisco_xe.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,8 @@ sync_network_data:
126126
parser: "textfsm"
127127
jpath: "[*].{local_interface:local_interface, remote_interface:neighbor_interface, remote_device:neighbor_name}"
128128
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 }}"
129+
software_version:
130+
commands:
131+
- command: "show version"
132+
parser: "textfsm"
133+
jpath: "[*].version"

nautobot_device_onboarding/command_mappers/juniper_junos.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,8 @@ sync_network_data:
121121
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
122122
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 }}"
123123
iterable_type: "dict"
124+
software_version:
125+
commands:
126+
- command: "show system information | display json"
127+
parser: "none"
128+
jpath: '"system-information"[]."os-version"[].data' # yamllint disable-line rule:quoted-strings

nautobot_device_onboarding/diffsync/adapters/sync_network_data_adapters.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.conf import settings
88
from django.contrib.contenttypes.models import ContentType
99
from django.core.exceptions import ValidationError
10-
from nautobot.dcim.models import Interface
10+
from nautobot.dcim.models import Device, Interface, SoftwareVersion
1111
from nautobot.ipam.models import VLAN, VRF, IPAddress
1212
from nautobot_ssot.contrib import NautobotAdapter
1313
from netaddr import EUI, mac_unix_expanded
@@ -52,6 +52,8 @@ class SyncNetworkDataNautobotAdapter(FilteredNautobotAdapter):
5252
lag_to_interface = sync_network_data_models.SyncNetworkDataLagToInterface
5353
vrf_to_interface = sync_network_data_models.SyncNetworkDataVrfToInterface
5454
cable = sync_network_data_models.SyncNetworkDataCable
55+
software_version = sync_network_data_models.SyncNetworkSoftwareVersion
56+
software_version_to_device = sync_network_data_models.SyncNetworkSoftwareVersionToDevice
5557

5658
primary_ips = None
5759

@@ -66,6 +68,8 @@ class SyncNetworkDataNautobotAdapter(FilteredNautobotAdapter):
6668
"lag_to_interface",
6769
"vrf_to_interface",
6870
"cable",
71+
"software_version",
72+
"software_version_to_device",
6973
]
7074

7175
def _cache_primary_ips(self, device_queryset):
@@ -305,6 +309,32 @@ def load_cables(self):
305309
except diffsync.exceptions.ObjectAlreadyExists:
306310
continue
307311

312+
def load_software_versions(self):
313+
"""Load Software Versions into the Diffsync store."""
314+
for software_version in SoftwareVersion.objects.all():
315+
network_software_version = self.software_version(
316+
adapter=self,
317+
version=software_version.version,
318+
platform__name=software_version.platform.name,
319+
)
320+
try:
321+
network_software_version.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST
322+
self.add(network_software_version)
323+
except diffsync.exceptions.ObjectAlreadyExists:
324+
continue
325+
326+
def load_software_version_to_device(self):
327+
"""Load Software Version to Device assignments into the Diffsync store."""
328+
for device in self.job.devices_to_load:
329+
network_software_version_to_device = self.software_version_to_device(
330+
adapter=self,
331+
name=device.name,
332+
serial=device.serial,
333+
software_version__version=device.software_version.version if device.software_version else "",
334+
)
335+
network_software_version_to_device.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST
336+
self.add(network_software_version_to_device)
337+
308338
def load(self):
309339
"""Generic implementation of the load function."""
310340
if not hasattr(self, "top_level") or not self.top_level:
@@ -334,6 +364,12 @@ def load(self):
334364
elif model_name == "cable":
335365
if self.job.sync_cables:
336366
self.load_cables()
367+
elif model_name == "software_version":
368+
if self.job.sync_software_version:
369+
self.load_software_versions()
370+
elif model_name == "software_version_to_device":
371+
if self.job.sync_software_version:
372+
self.load_software_version_to_device()
337373
else:
338374
diffsync_model = self._get_diffsync_class(model_name)
339375
self._load_objects(diffsync_model)
@@ -409,6 +445,8 @@ def __init__(self, *args, job, sync=None, **kwargs):
409445
lag_to_interface = sync_network_data_models.SyncNetworkDataLagToInterface
410446
vrf_to_interface = sync_network_data_models.SyncNetworkDataVrfToInterface
411447
cable = sync_network_data_models.SyncNetworkDataCable
448+
software_version = sync_network_data_models.SyncNetworkSoftwareVersion
449+
software_version_to_device = sync_network_data_models.SyncNetworkSoftwareVersionToDevice
412450

413451
top_level = [
414452
"ip_address",
@@ -421,6 +459,8 @@ def __init__(self, *args, job, sync=None, **kwargs):
421459
"lag_to_interface",
422460
"vrf_to_interface",
423461
"cable",
462+
"software_version",
463+
"software_version_to_device",
424464
]
425465

426466
def _handle_failed_devices(self, device_data):
@@ -890,6 +930,46 @@ def load_cables(self): # pylint: disable=inconsistent-return-statements
890930
model_type="cable",
891931
)
892932

933+
def load_software_versions(self):
934+
"""Load software versions into the Diffsync store."""
935+
for ( # pylint: disable=too-many-nested-blocks
936+
hostname,
937+
device_data,
938+
) in self.job.command_getter_result.items():
939+
if self.job.debug:
940+
self.job.logger.debug(f"Loading Software Versions from {hostname}")
941+
if device_data["software_version"]:
942+
device = Device.objects.get(serial=device_data["serial"])
943+
try:
944+
network_software_version = self.software_version(
945+
adapter=self,
946+
platform__name=device.platform.name,
947+
version=device_data["software_version"],
948+
)
949+
self.add(network_software_version)
950+
except diffsync.exceptions.ObjectAlreadyExists:
951+
continue
952+
953+
def load_software_version_to_device(self):
954+
"""Load software version to device assignments into the Diffsync store."""
955+
for ( # pylint: disable=too-many-nested-blocks
956+
hostname,
957+
device_data,
958+
) in self.job.command_getter_result.items():
959+
if self.job.debug:
960+
self.job.logger.debug(f"Loading Software Version to Device assignments from {hostname}")
961+
if device_data["software_version"]:
962+
try:
963+
network_software_version_to_device = self.software_version_to_device(
964+
adapter=self,
965+
name=hostname,
966+
serial=device_data["serial"],
967+
software_version__version=device_data["software_version"],
968+
)
969+
self.add(network_software_version_to_device)
970+
except diffsync.exceptions.ObjectAlreadyExists:
971+
continue
972+
893973
def load(self):
894974
"""Load network data."""
895975
self.execute_command_getter()
@@ -908,3 +988,7 @@ def load(self):
908988
self.load_vrf_to_interface()
909989
if self.job.sync_cables:
910990
self.load_cables()
991+
if self.job.sync_software_version:
992+
self.load_software_versions()
993+
if self.job.sync_software_version:
994+
self.load_software_version_to_device()

nautobot_device_onboarding/diffsync/models/sync_network_data_models.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Diffsync models."""
22

33
from typing import List, Optional
4+
from uuid import UUID
45

56
try:
67
from typing import Annotated # Python>=3.9
@@ -11,7 +12,7 @@
1112
from diffsync import exceptions as diffsync_exceptions
1213
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError
1314
from nautobot.dcim.choices import InterfaceTypeChoices
14-
from nautobot.dcim.models import Cable, Device, Interface, Location
15+
from nautobot.dcim.models import Cable, Device, Interface, Location, Platform, SoftwareVersion
1516
from nautobot.extras.models import Status
1617
from nautobot.ipam.models import VLAN, VRF, IPAddress, IPAddressToInterface
1718
from nautobot_ssot.contrib import CustomFieldAnnotation, NautobotModel
@@ -667,3 +668,117 @@ class SyncNetworkDataCable(FilteredNautobotModel):
667668
termination_b__name: str
668669

669670
status__name: str
671+
672+
673+
class SyncNetworkSoftwareVersion(DiffSyncModel):
674+
"""Shared data model representing a software version."""
675+
676+
_modelname = "software_version"
677+
_model = SoftwareVersion
678+
_identifiers = (
679+
"version",
680+
"platform__name",
681+
)
682+
_attributes = ()
683+
_children = {}
684+
685+
version: str
686+
platform__name: str
687+
688+
pk: Optional[UUID] = None
689+
690+
@classmethod
691+
def create(cls, adapter, ids, attrs):
692+
"""Create a new software version."""
693+
try:
694+
platform = Platform.objects.get(name=ids["platform__name"])
695+
except ObjectDoesNotExist:
696+
adapter.job.logger.error(
697+
f"Failed to create software version {ids['version']}. An platform with name: "
698+
f"{ids['platform__name']} was not found."
699+
)
700+
raise diffsync_exceptions.ObjectNotCreated
701+
try:
702+
software_version = SoftwareVersion(
703+
version=ids["version"],
704+
platform=platform,
705+
status=Status.objects.get(name="Active"),
706+
)
707+
software_version.validated_save()
708+
except ValidationError as err:
709+
adapter.job.logger.error(f"Software version {software_version} failed to create, {err}")
710+
raise diffsync_exceptions.ObjectNotCreated
711+
712+
return super().create(adapter, ids, attrs)
713+
714+
def delete(self):
715+
"""Prevent software version deletion."""
716+
self.adapter.job.logger.error(f"{self} will not be deleted.")
717+
return None
718+
719+
720+
class SyncNetworkSoftwareVersionToDevice(DiffSyncModel):
721+
"""Shared data model representing a software version to device."""
722+
723+
_model = Device
724+
_modelname = "software_version_to_device"
725+
_identifiers = (
726+
"name",
727+
"serial",
728+
)
729+
_attributes = ("software_version__version",)
730+
731+
name: str
732+
serial: str
733+
software_version__version: str
734+
735+
def _get_and_assign_sofware_version(self, adapter, attrs):
736+
"""Assign a software version to a device."""
737+
try:
738+
device = Device.objects.get(**self.get_identifiers())
739+
except ObjectDoesNotExist:
740+
adapter.job.logger.error(
741+
f"Failed to assign software version to {self.name}. An device with name: " f"{self.name} was not found."
742+
)
743+
raise diffsync_exceptions.ObjectNotCreated
744+
try:
745+
software_version = SoftwareVersion.objects.get(
746+
version=attrs["software_version__version"], platform=device.platform
747+
)
748+
device.software_version = software_version
749+
except ObjectDoesNotExist:
750+
adapter.job.logger.error(
751+
f"Failed to assign software version to {self.name}. An software version with name: "
752+
f"{self.name} was not found."
753+
)
754+
raise diffsync_exceptions.ObjectNotUpdated
755+
try:
756+
device.validated_save()
757+
except ValidationError as err:
758+
adapter.job.logger.error(f"Software version {software_version} failed to assign, {err}")
759+
raise diffsync_exceptions.ObjectNotUpdated
760+
761+
def update(self, attrs):
762+
"""Update an existing SoftwareVersionToDevice object."""
763+
if attrs.get("software_version__version"):
764+
try:
765+
self._get_and_assign_sofware_version(self.adapter, attrs)
766+
except ObjectDoesNotExist as err:
767+
self.adapter.job.logger.error(f"{self} failed to update, {err}")
768+
raise diffsync_exceptions.ObjectNotUpdated
769+
770+
return super().update(attrs)
771+
772+
@classmethod
773+
def create(cls, adapter, ids, attrs):
774+
"""
775+
Do not create new devices.
776+
777+
Network devices need to exist in Nautobot prior to syncing data and
778+
need to be included in the queryset generated based on job form inputs.
779+
"""
780+
return None
781+
782+
def delete(self):
783+
"""Prevent device deletion."""
784+
return None

0 commit comments

Comments
 (0)