Skip to content

Commit c59f98c

Browse files
authored
Merge pull request #469 from nautobot/dcates-add-device-content-type
automatically add the device content type to the site type
2 parents 2249595 + 2948ab9 commit c59f98c

File tree

4 files changed

+105
-1
lines changed

4 files changed

+105
-1
lines changed

changes/329.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Updated Sync Devices from Network job to automatically update location type content types if required

nautobot_device_onboarding/jobs.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
from nautobot_device_onboarding.nornir_plays.inventory_creator import _set_inventory
6262
from nautobot_device_onboarding.nornir_plays.logger import NornirLogger
6363
from nautobot_device_onboarding.nornir_plays.processor import TroubleshootingProcessor
64-
from nautobot_device_onboarding.utils.helper import onboarding_task_fqdn_to_ip
64+
from nautobot_device_onboarding.utils.helper import add_content_type, onboarding_task_fqdn_to_ip
6565

6666
InventoryPluginRegister.register("empty-inventory", EmptyInventory)
6767

@@ -382,6 +382,8 @@ def _process_csv_data(self, csv_file):
382382
self.logger.error("The CSV file contains no data!")
383383
return None
384384

385+
location_type_ids = set()
386+
385387
self.logger.info("Processing CSV data...")
386388
processing_failed = False
387389
processed_csv_data = {}
@@ -397,6 +399,14 @@ def _process_csv_data(self, csv_file):
397399
else:
398400
query = query = f"location_name: {row.get('location_name')}"
399401
location = Location.objects.get(name=row["location_name"].strip(), parent=None)
402+
403+
# Check and add content type if needed (only once per location type)
404+
location_type = location.location_type
405+
if location_type.id not in location_type_ids:
406+
if not location_type.content_types.filter(app_label="dcim", model="device").exists():
407+
add_content_type(self, model_to_add=Device, target_object=location_type)
408+
location_type_ids.add(location_type.id)
409+
400410
query = f"device_role: {row.get('device_role_name')}"
401411
device_role = Role.objects.get(
402412
name=row["device_role_name"].strip(),
@@ -577,6 +587,11 @@ def run( # pylint: disable=too-many-positional-arguments
577587
# TODO: We're only raising an exception if a csv file is not provided. Is that correct?
578588
raise ValueError("Platform.network_driver missing")
579589

590+
if location:
591+
location_type = location.location_type
592+
if not location_type.content_types.filter(app_label="dcim", model="device").exists():
593+
add_content_type(self, model_to_add=Device, target_object=location_type)
594+
580595
for ip_address in ip_addresses.replace(" ", "").split(","):
581596
resolved = self._validate_ip_address(ip_address)
582597
self.ip_address_inventory[resolved] = {"original_ip_address": ip_address, **default_values}

nautobot_device_onboarding/tests/test_jobs.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,68 @@ def test_csv_process_pass_connectivity_test_flag(self, mock_sync_devices_command
184184
self.assertEqual(job.ip_address_inventory["172.23.0.8"]["platform"], None)
185185
self.assertEqual(log_level, 10)
186186

187+
def test_add_content_type_during_csv_sync(self):
188+
"""Test successful addition of content type to location type during CSV sync."""
189+
# Create a location type without Device content type
190+
location_type_without_device = self.testing_objects["location_2"].location_type
191+
location_type_without_device.content_types.clear()
192+
location_type_without_device.validated_save()
193+
194+
self.assertFalse(location_type_without_device.content_types.filter(app_label="dcim", model="device").exists())
195+
196+
# Run CSV processing which should add the content type
197+
onboarding_job = jobs.SSOTSyncDevices()
198+
with open("nautobot_device_onboarding/tests/fixtures/onboarding_csv_fixture.csv", "rb") as csv_file:
199+
onboarding_job._process_csv_data(csv_file=csv_file) # pylint: disable=protected-access
200+
201+
# Verify content type was added
202+
location_type_without_device.refresh_from_db()
203+
self.assertTrue(location_type_without_device.content_types.filter(app_label="dcim", model="device").exists())
204+
205+
@patch("nautobot_device_onboarding.diffsync.adapters.sync_devices_adapters.sync_devices_command_getter")
206+
def test_add_content_type_during_manual_sync(self, device_data):
207+
"""Test that content type is added when running manual sync with location."""
208+
device_data.return_value = sync_devices_fixture.sync_devices_mock_data_valid
209+
210+
# Create a location type without Device content type
211+
location_type_without_device = self.testing_objects["location_2"].location_type
212+
location_type_without_device.content_types.clear()
213+
location_type_without_device.validated_save()
214+
215+
self.assertFalse(location_type_without_device.content_types.filter(app_label="dcim", model="device").exists())
216+
217+
job_form_inputs = {
218+
"debug": True,
219+
"connectivity_test": False,
220+
"dryrun": False,
221+
"csv_file": None,
222+
"location": self.testing_objects["location_2"].pk,
223+
"namespace": self.testing_objects["namespace"].pk,
224+
"ip_addresses": "10.1.1.10,10.1.1.11",
225+
"port": 22,
226+
"timeout": 30,
227+
"set_mgmt_only": True,
228+
"update_devices_without_primary_ip": True,
229+
"device_role": self.testing_objects["device_role"].pk,
230+
"device_status": self.testing_objects["status"].pk,
231+
"interface_status": self.testing_objects["status"].pk,
232+
"ip_address_status": self.testing_objects["status"].pk,
233+
"secrets_group": self.testing_objects["secrets_group"].pk,
234+
"platform": None,
235+
"memory_profiling": False,
236+
}
237+
job_result = create_job_result_and_run_job(
238+
module="nautobot_device_onboarding.jobs", name="SSOTSyncDevices", **job_form_inputs
239+
)
240+
self.assertEqual(
241+
job_result.status,
242+
JobResultStatusChoices.STATUS_SUCCESS,
243+
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
244+
)
245+
# Verify content type was added
246+
location_type_without_device.refresh_from_db()
247+
self.assertTrue(location_type_without_device.content_types.filter(app_label="dcim", model="device").exists())
248+
187249

188250
class SSOTSyncNetworkDataTestCase(TransactionTestCase):
189251
"""Test SSOTSyncNetworkData class."""

nautobot_device_onboarding/utils/helper.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import socket
66

77
import netaddr
8+
from django.contrib.contenttypes.models import ContentType
89
from django.db import connections
910
from nautobot.dcim.filters import DeviceFilterSet
1011
from nautobot.dcim.models import Device
@@ -132,3 +133,28 @@ def inner(*args, **kwargs):
132133
connections.close_all()
133134

134135
return inner
136+
137+
138+
def add_content_type(job, model_to_add, target_object):
139+
"""Add a content type to the valid content types of a target object.
140+
141+
Args:
142+
job: The job object used for logging.
143+
model_to_add: The model class to get the content type for.
144+
target_object: The object to which the content type will be added.
145+
146+
Raises:
147+
OnboardException: If adding the content type fails.
148+
"""
149+
try:
150+
job.logger.info(
151+
"Adding %s content type to valid content types for location type %s",
152+
model_to_add.__name__,
153+
target_object,
154+
)
155+
content_type = ContentType.objects.get_for_model(model_to_add)
156+
target_object.content_types.add(content_type)
157+
except Exception as e:
158+
err_msg = f"Failed to add {model_to_add.__name__} to valid content types for {target_object}: {e}"
159+
job.logger.error(err_msg)
160+
raise OnboardException("fail-general - " + err_msg) from e

0 commit comments

Comments
 (0)