Skip to content
Merged
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
113 changes: 113 additions & 0 deletions python/understack-workflows/tests/test_nautobot_device_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,119 @@ def test_sync_without_location_returns_error(

assert result == EXIT_STATUS_FAILURE

@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
@patch(
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
)
def test_sync_finds_device_by_name_with_matching_uuid(
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
):
"""Test that device found by name with matching UUID is updated."""
node_uuid = str(uuid.uuid4())
device_info = DeviceInfo(
uuid=node_uuid,
name="Dell-ABC123",
manufacturer="Dell",
model="PowerEdge R640",
location_id="location-uuid",
status="Active",
)
mock_fetch.return_value = (device_info, {}, [])

# First get by ID returns None
# Second get by name returns device with same UUID
existing_device = MagicMock()
existing_device.id = node_uuid # Same UUID
existing_device.status = MagicMock(name="Planned")
existing_device.name = "Dell-ABC123"
existing_device.serial = None
existing_device.location = None
existing_device.rack = None
existing_device.tenant = None
existing_device.custom_fields = {}

mock_nautobot.dcim.devices.get.side_effect = [None, existing_device]
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS

result = sync_device_to_nautobot(node_uuid, mock_nautobot)

assert result == EXIT_STATUS_SUCCESS
# Should NOT delete since UUIDs match
existing_device.delete.assert_not_called()
# Should NOT create new device
mock_nautobot.dcim.devices.create.assert_not_called()

@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
@patch(
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
)
def test_sync_recreates_device_with_mismatched_uuid(
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
):
"""Test device with mismatched UUID is deleted and recreated."""
node_uuid = str(uuid.uuid4())
old_uuid = str(uuid.uuid4()) # Different UUID
device_info = DeviceInfo(
uuid=node_uuid,
name="Dell-ABC123",
manufacturer="Dell",
model="PowerEdge R640",
location_id="location-uuid",
status="Active",
)
mock_fetch.return_value = (device_info, {}, [])

# First get by ID returns None
# Second get by name returns device with different UUID
existing_device = MagicMock()
existing_device.id = old_uuid # Different UUID
existing_device.status = MagicMock(name="Planned")
existing_device.name = "Dell-ABC123"

mock_nautobot.dcim.devices.get.side_effect = [None, existing_device]
mock_nautobot.dcim.devices.create.return_value = MagicMock()
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS

result = sync_device_to_nautobot(node_uuid, mock_nautobot)

assert result == EXIT_STATUS_SUCCESS
# Should delete old device
existing_device.delete.assert_called_once()
# Should create new device with correct UUID
mock_nautobot.dcim.devices.create.assert_called_once()

@patch("understack_workflows.oslo_event.nautobot_device_sync.IronicClient")
@patch("understack_workflows.oslo_event.nautobot_device_sync.fetch_device_info")
@patch(
"understack_workflows.oslo_event.nautobot_device_sync.sync_interfaces_from_data"
)
def test_sync_device_not_found_by_name_creates_new(
self, mock_sync_interfaces, mock_fetch, mock_ironic_class, mock_nautobot
):
"""Test that device not found by UUID or name is created."""
node_uuid = str(uuid.uuid4())
device_info = DeviceInfo(
uuid=node_uuid,
name="Dell-ABC123",
manufacturer="Dell",
model="PowerEdge R640",
location_id="location-uuid",
status="Active",
)
mock_fetch.return_value = (device_info, {}, [])

# Both lookups return None
mock_nautobot.dcim.devices.get.side_effect = [None, None]
mock_nautobot.dcim.devices.create.return_value = MagicMock()
mock_sync_interfaces.return_value = EXIT_STATUS_SUCCESS

result = sync_device_to_nautobot(node_uuid, mock_nautobot)

assert result == EXIT_STATUS_SUCCESS
mock_nautobot.dcim.devices.create.assert_called_once()


class TestDeleteDeviceFromNautobot:
"""Test cases for delete_device_from_nautobot function."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,31 @@ def sync_device_to_nautobot(
# Check if device exists in Nautobot
nautobot_device = nautobot_client.dcim.devices.get(id=device_info.uuid)

if not nautobot_device:
# Try finding by name (handles re-enrollment scenarios)
if device_info.name:
nautobot_device = nautobot_client.dcim.devices.get(
name=device_info.name
)
if nautobot_device and not isinstance(nautobot_device, list):
logger.info(
"Found existing device by name %s with ID %s, "
"will recreate with UUID %s",
device_info.name,
nautobot_device.id,
device_info.uuid,
)
if str(nautobot_device.id) != device_info.uuid:
logger.warning(
"Device %s has mismatched UUID (Nautobot: %s, Ironic: %s), "
"recreating",
device_info.name,
nautobot_device.id,
device_info.uuid,
)
nautobot_device.delete()
nautobot_device = None # Will trigger creation below

if not nautobot_device:
# Create new device with minimal fields
if not device_info.location_id:
Expand Down
Loading