Skip to content

Commit f544403

Browse files
committed
oslo_event(ironic node update): add tests
1 parent 5682630 commit f544403

File tree

1 file changed

+392
-0
lines changed

1 file changed

+392
-0
lines changed
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
import json
2+
import uuid
3+
from unittest.mock import MagicMock
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from understack_workflows.oslo_event.ironic_node import IronicProvisionSetEvent
9+
from understack_workflows.oslo_event.ironic_node import create_volume_connector
10+
from understack_workflows.oslo_event.ironic_node import handle_provision_end
11+
from understack_workflows.oslo_event.ironic_node import instance_nqn
12+
13+
14+
class TestIronicProvisionSetEvent:
15+
"""Test cases for IronicProvisionSetEvent class."""
16+
17+
def test_from_event_dict_success(self):
18+
"""Test successful event parsing with real JSON data."""
19+
# Load real event data from JSON sample
20+
json_file = (
21+
"tests/json_samples/ironic_versioned_notifications_server_provisioned.json"
22+
)
23+
with open(json_file) as f:
24+
sample_data = json.load(f)
25+
26+
# Parse the oslo message to get the actual event data
27+
oslo_message = json.loads(sample_data["oslomessage"])
28+
29+
# Create event data in the format expected by from_event_dict
30+
ironic_data = oslo_message["payload"]["ironic_object.data"]
31+
event_data = {
32+
"instance_uuid": ironic_data["instance_uuid"],
33+
"payload": oslo_message["payload"],
34+
}
35+
36+
event = IronicProvisionSetEvent.from_event_dict(event_data)
37+
38+
# Verify the parsed values match the real data
39+
# UUIDs are formatted with hyphens when converted to UUID objects
40+
assert str(event.owner) == "32e02632-f4f0-4415-bab5-895d1e7247b7"
41+
assert str(event.lessee) == "5f5955bc-89e1-48e5-9a12-110a3945e4d7"
42+
assert str(event.instance_uuid) == "5027885e-52a8-48f9-adf4-14d8f5f4ccb8"
43+
assert str(event.node_uuid) == "461737c4-037c-41bf-9c17-f4f33ff20dd7"
44+
assert event.event == "done"
45+
46+
def test_from_event_dict_no_payload(self):
47+
"""Test event parsing with missing payload."""
48+
event_data = {"instance_uuid": uuid.uuid4()}
49+
50+
with pytest.raises(ValueError, match="invalid event"):
51+
IronicProvisionSetEvent.from_event_dict(event_data)
52+
53+
def test_from_event_dict_no_ironic_object_data(self):
54+
"""Test event parsing with missing ironic_object.data."""
55+
event_data = {
56+
"instance_uuid": uuid.uuid4(),
57+
"payload": {"other_field": "value"},
58+
}
59+
60+
with pytest.raises(
61+
ValueError, match="Invalid event. No 'ironic_object.data' in payload"
62+
):
63+
IronicProvisionSetEvent.from_event_dict(event_data)
64+
65+
def test_from_event_dict_missing_required_fields(self):
66+
"""Test event parsing with missing required fields in ironic_object.data."""
67+
event_data = {
68+
"instance_uuid": uuid.uuid4(),
69+
"payload": {
70+
"ironic_object.data": {
71+
"owner": uuid.uuid4(),
72+
# Missing lessee, event, uuid
73+
}
74+
},
75+
}
76+
77+
with pytest.raises(KeyError):
78+
IronicProvisionSetEvent.from_event_dict(event_data)
79+
80+
def test_direct_initialization(self):
81+
"""Test direct initialization of IronicProvisionSetEvent."""
82+
owner_uuid = uuid.uuid4()
83+
lessee_uuid = uuid.uuid4()
84+
instance_uuid = uuid.uuid4()
85+
node_uuid = uuid.uuid4()
86+
event_type = "provision_end"
87+
88+
event = IronicProvisionSetEvent(
89+
owner=owner_uuid,
90+
lessee=lessee_uuid,
91+
instance_uuid=instance_uuid,
92+
node_uuid=node_uuid,
93+
event=event_type,
94+
)
95+
96+
assert event.owner == owner_uuid
97+
assert event.lessee == lessee_uuid
98+
assert event.instance_uuid == instance_uuid
99+
assert event.node_uuid == node_uuid
100+
assert event.event == event_type
101+
102+
103+
class TestHandleProvisionEnd:
104+
"""Test cases for handle_provision_end function."""
105+
106+
@pytest.fixture
107+
def mock_conn(self):
108+
"""Create a mock OpenStack connection."""
109+
return MagicMock()
110+
111+
@pytest.fixture
112+
def mock_nautobot(self):
113+
"""Create a mock Nautobot instance."""
114+
return MagicMock()
115+
116+
@pytest.fixture
117+
def valid_event_data(self):
118+
"""Create valid event data for testing."""
119+
return {
120+
"payload": {
121+
"ironic_object.data": {
122+
"instance_uuid": uuid.uuid4(),
123+
"owner": uuid.uuid4(),
124+
"lessee": uuid.uuid4(),
125+
"event": "provision_end",
126+
"uuid": uuid.uuid4(),
127+
}
128+
},
129+
}
130+
131+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
132+
def test_handle_provision_end_project_not_svm_enabled(
133+
self, mock_is_svm_enabled, mock_conn, mock_nautobot, valid_event_data
134+
):
135+
"""Test handling when project is not SVM enabled."""
136+
mock_is_svm_enabled.return_value = False
137+
138+
result = handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
139+
140+
assert result == 0
141+
lessee_uuid = valid_event_data["payload"]["ironic_object.data"]["lessee"]
142+
mock_is_svm_enabled.assert_called_once_with(mock_conn, str(lessee_uuid.hex))
143+
144+
@patch("understack_workflows.oslo_event.ironic_node.create_volume_connector")
145+
@patch("understack_workflows.oslo_event.ironic_node.save_output")
146+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
147+
def test_handle_provision_end_server_not_found(
148+
self,
149+
mock_is_svm_enabled,
150+
mock_save_output,
151+
mock_create_connector,
152+
mock_conn,
153+
mock_nautobot,
154+
valid_event_data,
155+
):
156+
"""Test handling when server is not found."""
157+
mock_is_svm_enabled.return_value = True
158+
mock_conn.get_server_by_id.return_value = None
159+
160+
result = handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
161+
162+
assert result == 1
163+
instance_uuid = valid_event_data["payload"]["ironic_object.data"][
164+
"instance_uuid"
165+
]
166+
mock_conn.get_server_by_id.assert_called_once_with(instance_uuid)
167+
mock_save_output.assert_called_once_with("storage", "not-found")
168+
mock_create_connector.assert_not_called()
169+
170+
@patch("understack_workflows.oslo_event.ironic_node.create_volume_connector")
171+
@patch("understack_workflows.oslo_event.ironic_node.save_output")
172+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
173+
def test_handle_provision_end_storage_wanted(
174+
self,
175+
mock_is_svm_enabled,
176+
mock_save_output,
177+
mock_create_connector,
178+
mock_conn,
179+
mock_nautobot,
180+
valid_event_data,
181+
):
182+
"""Test handling when server wants storage."""
183+
mock_is_svm_enabled.return_value = True
184+
mock_server = MagicMock()
185+
mock_server.id = "server-123"
186+
mock_server.metadata = {"storage": "wanted"}
187+
mock_conn.get_server_by_id.return_value = mock_server
188+
189+
result = handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
190+
191+
assert result == 0
192+
ironic_data = valid_event_data["payload"]["ironic_object.data"]
193+
instance_uuid = ironic_data["instance_uuid"]
194+
node_uuid = ironic_data["uuid"]
195+
196+
mock_conn.get_server_by_id.assert_called_once_with(instance_uuid)
197+
198+
# Check save_output calls
199+
expected_calls = [
200+
("storage", "wanted"),
201+
("node_uuid", str(node_uuid)),
202+
("instance_uuid", str(instance_uuid)),
203+
]
204+
actual_calls = [call.args for call in mock_save_output.call_args_list]
205+
assert actual_calls == expected_calls
206+
207+
mock_create_connector.assert_called_once()
208+
209+
@patch("understack_workflows.oslo_event.ironic_node.create_volume_connector")
210+
@patch("understack_workflows.oslo_event.ironic_node.save_output")
211+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
212+
def test_handle_provision_end_storage_not_wanted(
213+
self,
214+
mock_is_svm_enabled,
215+
mock_save_output,
216+
mock_create_connector,
217+
mock_conn,
218+
mock_nautobot,
219+
valid_event_data,
220+
):
221+
"""Test handling when server does not want storage."""
222+
mock_is_svm_enabled.return_value = True
223+
mock_server = MagicMock()
224+
mock_server.id = "server-123"
225+
mock_server.metadata = {"storage": "not-wanted"}
226+
mock_conn.get_server_by_id.return_value = mock_server
227+
228+
result = handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
229+
230+
assert result == 0
231+
ironic_data = valid_event_data["payload"]["ironic_object.data"]
232+
instance_uuid = ironic_data["instance_uuid"]
233+
node_uuid = ironic_data["uuid"]
234+
235+
mock_conn.get_server_by_id.assert_called_once_with(instance_uuid)
236+
237+
# Check save_output calls
238+
expected_calls = [
239+
("storage", "not-set"),
240+
("node_uuid", str(node_uuid)),
241+
("instance_uuid", str(instance_uuid)),
242+
]
243+
actual_calls = [call.args for call in mock_save_output.call_args_list]
244+
assert actual_calls == expected_calls
245+
246+
mock_create_connector.assert_called_once()
247+
248+
@patch("understack_workflows.oslo_event.ironic_node.create_volume_connector")
249+
@patch("understack_workflows.oslo_event.ironic_node.save_output")
250+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
251+
def test_handle_provision_end_storage_metadata_missing(
252+
self,
253+
mock_is_svm_enabled,
254+
mock_save_output,
255+
mock_create_connector,
256+
mock_conn,
257+
mock_nautobot,
258+
valid_event_data,
259+
):
260+
"""Test handling when server metadata doesn't have storage key."""
261+
mock_is_svm_enabled.return_value = True
262+
mock_server = MagicMock()
263+
mock_server.id = "server-123"
264+
mock_server.metadata = {"other_key": "value"}
265+
mock_conn.get_server_by_id.return_value = mock_server
266+
267+
# This should raise a KeyError when accessing metadata["storage"]
268+
with pytest.raises(KeyError):
269+
handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
270+
271+
@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
272+
def test_handle_provision_end_invalid_event_data(
273+
self, mock_is_svm_enabled, mock_conn, mock_nautobot
274+
):
275+
"""Test handling with invalid event data."""
276+
invalid_event_data = {"instance_uuid": uuid.uuid4(), "payload": {}}
277+
278+
with pytest.raises(
279+
ValueError, match="Invalid event. No 'ironic_object.data' in payload"
280+
):
281+
handle_provision_end(mock_conn, mock_nautobot, invalid_event_data)
282+
283+
mock_is_svm_enabled.assert_not_called()
284+
285+
286+
class TestCreateVolumeConnector:
287+
"""Test cases for create_volume_connector function."""
288+
289+
@pytest.fixture
290+
def mock_conn(self):
291+
"""Create a mock OpenStack connection."""
292+
return MagicMock()
293+
294+
@pytest.fixture
295+
def sample_event(self):
296+
"""Create a sample IronicProvisionSetEvent."""
297+
return IronicProvisionSetEvent(
298+
owner=uuid.uuid4(),
299+
lessee=uuid.uuid4(),
300+
instance_uuid=uuid.uuid4(),
301+
node_uuid=uuid.uuid4(),
302+
event="provision_end",
303+
)
304+
305+
def test_create_volume_connector_success(self, mock_conn, sample_event):
306+
"""Test successful volume connector creation."""
307+
mock_connector = MagicMock()
308+
mock_conn.baremetal.create_volume_connector.return_value = mock_connector
309+
310+
result = create_volume_connector(mock_conn, sample_event)
311+
312+
assert result == mock_connector
313+
expected_nqn = f"nqn.2014-08.org.nvmexpress:uuid:{sample_event.instance_uuid}"
314+
mock_conn.baremetal.create_volume_connector.assert_called_once_with(
315+
node_uuid=sample_event.node_uuid,
316+
type="iqn",
317+
connector_id=expected_nqn,
318+
)
319+
320+
def test_create_volume_connector_with_different_instance_uuid(
321+
self, mock_conn, sample_event
322+
):
323+
"""Test volume connector creation with different instance UUID."""
324+
different_instance_uuid = uuid.uuid4()
325+
sample_event.instance_uuid = different_instance_uuid
326+
327+
mock_connector = MagicMock()
328+
mock_conn.baremetal.create_volume_connector.return_value = mock_connector
329+
330+
result = create_volume_connector(mock_conn, sample_event)
331+
332+
assert result == mock_connector
333+
expected_nqn = f"nqn.2014-08.org.nvmexpress:uuid:{different_instance_uuid}"
334+
mock_conn.baremetal.create_volume_connector.assert_called_once_with(
335+
node_uuid=sample_event.node_uuid,
336+
type="iqn",
337+
connector_id=expected_nqn,
338+
)
339+
340+
def test_create_volume_connector_baremetal_exception(self, mock_conn, sample_event):
341+
"""Test handling when baremetal connector creation fails."""
342+
mock_conn.baremetal.create_volume_connector.side_effect = Exception(
343+
"Baremetal service error"
344+
)
345+
346+
with pytest.raises(Exception, match="Baremetal service error"):
347+
create_volume_connector(mock_conn, sample_event)
348+
349+
mock_conn.baremetal.create_volume_connector.assert_called_once()
350+
351+
352+
class TestInstanceNqn:
353+
"""Test cases for instance_nqn function."""
354+
355+
def test_instance_nqn_format(self):
356+
"""Test NQN format generation."""
357+
test_uuid = uuid.uuid4()
358+
expected_nqn = f"nqn.2014-08.org.nvmexpress:uuid:{test_uuid}"
359+
360+
result = instance_nqn(test_uuid)
361+
362+
assert result == expected_nqn
363+
364+
def test_instance_nqn_different_uuids(self):
365+
"""Test NQN generation with different UUIDs."""
366+
uuid1 = uuid.uuid4()
367+
uuid2 = uuid.uuid4()
368+
369+
nqn1 = instance_nqn(uuid1)
370+
nqn2 = instance_nqn(uuid2)
371+
372+
assert nqn1 != nqn2
373+
assert nqn1 == f"nqn.2014-08.org.nvmexpress:uuid:{uuid1}"
374+
assert nqn2 == f"nqn.2014-08.org.nvmexpress:uuid:{uuid2}"
375+
376+
def test_instance_nqn_prefix_constant(self):
377+
"""Test that NQN prefix is consistent."""
378+
test_uuid = uuid.uuid4()
379+
result = instance_nqn(test_uuid)
380+
381+
assert result.startswith("nqn.2014-08.org.nvmexpress:uuid:")
382+
assert str(test_uuid) in result
383+
384+
def test_instance_nqn_with_known_uuid(self):
385+
"""Test NQN generation with a known UUID string."""
386+
known_uuid_str = "12345678-1234-5678-9abc-123456789abc"
387+
known_uuid = uuid.UUID(known_uuid_str)
388+
expected_nqn = f"nqn.2014-08.org.nvmexpress:uuid:{known_uuid_str}"
389+
390+
result = instance_nqn(known_uuid)
391+
392+
assert result == expected_nqn

0 commit comments

Comments
 (0)