Skip to content

Commit f73fe37

Browse files
fix(nautobot-sync): handle device name conflicts with UUID mismatch
When a device exists in Nautobot with the same name but a different UUID (e.g., from a previous enrollment or manual creation), the sync would fail with "A device with this name already exists" error. This change adds fallback logic to: 1. Look up device by name if not found by UUID 2. If found with mismatched UUID, delete and recreate with correct UUID
1 parent 98f2598 commit f73fe37

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

python/understack-workflows/tests/test_nautobot_device_sync.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,119 @@ def test_sync_without_location_returns_error(
520520

521521
assert result == EXIT_STATUS_FAILURE
522522

523+
@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
524+
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
525+
@patch(
526+
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
527+
)
528+
def test_sync_finds_device_by_name_with_matching_uuid(
529+
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
530+
):
531+
"""Test that device found by name with matching UUID is updated."""
532+
node_uuid = str(uuid.uuid4())
533+
device_info = DeviceInfo(
534+
uuid=node_uuid,
535+
name="Dell-ABC123",
536+
manufacturer="Dell",
537+
model="PowerEdge R640",
538+
location_id="location-uuid",
539+
status="Active",
540+
)
541+
mock_fetch.return_value = (device_info, {}, [])
542+
543+
# First get by ID returns None
544+
# Second get by name returns device with same UUID
545+
existing_device = MagicMock()
546+
existing_device.id = node_uuid # Same UUID
547+
existing_device.status = MagicMock(name="Planned")
548+
existing_device.name = "Dell-ABC123"
549+
existing_device.serial = None
550+
existing_device.location = None
551+
existing_device.rack = None
552+
existing_device.tenant = None
553+
existing_device.custom_fields = {}
554+
555+
mock_nautobot.dcim.devices.get.side_effect = [None, existing_device]
556+
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS
557+
558+
result = sync_device_to_nautobot(node_uuid, mock_nautobot)
559+
560+
assert result == EXIT_STATUS_SUCCESS
561+
# Should NOT delete since UUIDs match
562+
existing_device.delete.assert_not_called()
563+
# Should NOT create new device
564+
mock_nautobot.dcim.devices.create.assert_not_called()
565+
566+
@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
567+
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
568+
@patch(
569+
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
570+
)
571+
def test_sync_recreates_device_with_mismatched_uuid(
572+
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
573+
):
574+
"""Test device with mismatched UUID is deleted and recreated."""
575+
node_uuid = str(uuid.uuid4())
576+
old_uuid = str(uuid.uuid4()) # Different UUID
577+
device_info = DeviceInfo(
578+
uuid=node_uuid,
579+
name="Dell-ABC123",
580+
manufacturer="Dell",
581+
model="PowerEdge R640",
582+
location_id="location-uuid",
583+
status="Active",
584+
)
585+
mock_fetch.return_value = (device_info, {}, [])
586+
587+
# First get by ID returns None
588+
# Second get by name returns device with different UUID
589+
existing_device = MagicMock()
590+
existing_device.id = old_uuid # Different UUID
591+
existing_device.status = MagicMock(name="Planned")
592+
existing_device.name = "Dell-ABC123"
593+
594+
mock_nautobot.dcim.devices.get.side_effect = [None, existing_device]
595+
mock_nautobot.dcim.devices.create.return_value = MagicMock()
596+
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS
597+
598+
result = sync_device_to_nautobot(node_uuid, mock_nautobot)
599+
600+
assert result == EXIT_STATUS_SUCCESS
601+
# Should delete old device
602+
existing_device.delete.assert_called_once()
603+
# Should create new device with correct UUID
604+
mock_nautobot.dcim.devices.create.assert_called_once()
605+
606+
@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
607+
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
608+
@patch(
609+
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
610+
)
611+
def test_sync_device_not_found_by_name_creates_new(
612+
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
613+
):
614+
"""Test that device not found by UUID or name is created."""
615+
node_uuid = str(uuid.uuid4())
616+
device_info = DeviceInfo(
617+
uuid=node_uuid,
618+
name="Dell-ABC123",
619+
manufacturer="Dell",
620+
model="PowerEdge R640",
621+
location_id="location-uuid",
622+
status="Active",
623+
)
624+
mock_fetch.return_value = (device_info, {}, [])
625+
626+
# Both lookups return None
627+
mock_nautobot.dcim.devices.get.side_effect = [None, None]
628+
mock_nautobot.dcim.devices.create.return_value = MagicMock()
629+
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS
630+
631+
result = sync_device_to_nautobot(node_uuid, mock_nautobot)
632+
633+
assert result == EXIT_STATUS_SUCCESS
634+
mock_nautobot.dcim.devices.create.assert_called_once()
635+
523636

524637
class TestDeleteDeviceFromNautobot:
525638
"""Test cases for delete_device_from_nautobot function."""

python/understack-workflows/understack_workflows/oslo_event/nautobot_device_sync.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,31 @@ def sync_device_to_nautobot(
414414
# Check if device exists in Nautobot
415415
nautobot_device = nautobot_client.dcim.devices.get(id=device_info.uuid)
416416

417+
if not nautobot_device:
418+
# Try finding by name (handles re-enrollment scenarios)
419+
if device_info.name:
420+
nautobot_device = nautobot_client.dcim.devices.get(
421+
name=device_info.name
422+
)
423+
if nautobot_device and not isinstance(nautobot_device, list):
424+
logger.info(
425+
"Found existing device by name %s with ID %s, "
426+
"will recreate with UUID %s",
427+
device_info.name,
428+
nautobot_device.id,
429+
device_info.uuid,
430+
)
431+
if str(nautobot_device.id) != device_info.uuid:
432+
logger.warning(
433+
"Device %s has mismatched UUID (Nautobot: %s, Ironic: %s), "
434+
"recreating",
435+
device_info.name,
436+
nautobot_device.id,
437+
device_info.uuid,
438+
)
439+
nautobot_device.delete()
440+
nautobot_device = None # Will trigger creation below
441+
417442
if not nautobot_device:
418443
# Create new device with minimal fields
419444
if not device_info.location_id:

0 commit comments

Comments
 (0)