Skip to content

Commit b758377

Browse files
committed
create_svm: add support for identity.project.update
From now on, when the project is updated we look if the UNDERSTACK_SVM is present on the project. If it isn't - we log an error message and exit. If it is - we check if the SVM exists on the Netapp and create it if it doesn't.
1 parent f2b3157 commit b758377

File tree

6 files changed

+333
-8
lines changed

6 files changed

+333
-8
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"oslo.version": "2.0",
3+
"oslo.message": "{\"message_id\": \"e242b7f2-8228-40ec-9b34-234aee8353bc\", \"publisher_id\": \"identity.keystone-api-795dc7d644-nmm8j\", \"event_type\": \"identity.project.updated\", \"priority\": \"INFO\", \"payload\": {\"typeURI\": \"http://schemas.dmtf.org/cloud/audit/1.0/event\", \"eventType\": \"activity\", \"id\": \"c51b84e2-c4d1-5b0c-bcf1-c91d23002cdc\", \"eventTime\": \"2025-08-21T16:42:50.136659+0000\", \"action\": \"updated.project\", \"outcome\": \"success\", \"observer\": {\"id\": \"ad1c83a7f5f746d2a04fcc8dda226368\", \"typeURI\": \"service/security\"}, \"initiator\": {\"id\": \"e27a1fed41fd51a09221b35a38f7be23\", \"typeURI\": \"service/security/account/user\", \"host\": {\"address\": \"10.2.148.161\", \"agent\": \"python-keystoneclient\"}, \"user_id\": \"5f9179b7e200cd85c55e8a26400e266c6b4f7209f6d3fb2adc3cf8e1113c378c\", \"project_id\": \"32e02632f4f04415bab5895d1e7247b7\", \"request_id\": \"req-1965a371-44ea-4d60-b655-33b3be5caedc\", \"username\": \"[email protected]\"}, \"target\": {\"id\": \"de16994fb2124504b6b3d3623d1dba51\", \"typeURI\": \"data/security/project\"}, \"resource_info\": \"de16994fb2124504b6b3d3623d1dba51\"}, \"timestamp\": \"2025-08-21 16:42:50.137603\", \"_unique_id\": \"7423a282fbcc492dbb00a5700bce4249\"}"
4+
}

python/understack-workflows/tests/test_keystone_project.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from unittest.mock import MagicMock
2+
from unittest.mock import call
23
from unittest.mock import patch
34

45
import pytest
@@ -9,6 +10,7 @@
910
from understack_workflows.oslo_event.keystone_project import KeystoneProjectEvent
1011
from understack_workflows.oslo_event.keystone_project import _keystone_project_tags
1112
from understack_workflows.oslo_event.keystone_project import handle_project_created
13+
from understack_workflows.oslo_event.keystone_project import handle_project_updated
1214

1315

1416
class TestKeystoneProjectEvent:
@@ -288,3 +290,267 @@ def test_handle_project_created_constants_used(
288290
aggregate_name="aggr02_n02_NVME", # AGGREGATE_NAME constant
289291
)
290292
mock_open.assert_called()
293+
294+
295+
class TestHandleProjectUpdated:
296+
"""Test cases for handle_project_updated function."""
297+
298+
@pytest.fixture
299+
def mock_conn(self):
300+
"""Create a mock OpenStack connection."""
301+
return MagicMock()
302+
303+
@pytest.fixture
304+
def mock_nautobot(self):
305+
"""Create a mock Nautobot instance."""
306+
return MagicMock()
307+
308+
@pytest.fixture
309+
def valid_update_event_data(self):
310+
"""Create valid update event data for testing."""
311+
return {
312+
"event_type": "identity.project.updated",
313+
"payload": {"target": {"id": "test-project-123"}},
314+
}
315+
316+
def test_handle_project_updated_wrong_event_type(self, mock_conn, mock_nautobot):
317+
"""Test handling event with wrong event type."""
318+
event_data = {
319+
"event_type": "identity.project.created",
320+
"payload": {"target": {"id": "test-project-123"}},
321+
}
322+
323+
result = handle_project_updated(mock_conn, mock_nautobot, event_data)
324+
assert result == 1
325+
326+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
327+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
328+
@patch("builtins.open")
329+
def test_handle_project_updated_svm_tag_added(
330+
self,
331+
mock_open,
332+
mock_tags,
333+
mock_netapp_class,
334+
mock_conn,
335+
mock_nautobot,
336+
valid_update_event_data,
337+
):
338+
"""Test project update when SVM_UNDERSTACK tag is added."""
339+
# Project now has SVM tag
340+
mock_tags.return_value = ["tag1", SVM_PROJECT_TAG, "tag2"]
341+
342+
mock_netapp_manager = MagicMock()
343+
mock_netapp_manager.check_if_svm_exists.return_value = (
344+
False # SVM doesn't exist yet
345+
)
346+
mock_netapp_manager.create_svm.return_value = "os-test-project-123"
347+
mock_netapp_class.return_value = mock_netapp_manager
348+
349+
result = handle_project_updated(
350+
mock_conn, mock_nautobot, valid_update_event_data
351+
)
352+
353+
assert result == 0
354+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
355+
mock_netapp_manager.check_if_svm_exists.assert_called_once_with(
356+
project_id="test-project-123"
357+
)
358+
mock_netapp_manager.create_svm.assert_called_once_with(
359+
project_id="test-project-123", aggregate_name=AGGREGATE_NAME
360+
)
361+
mock_netapp_manager.create_volume.assert_called_once_with(
362+
project_id="test-project-123",
363+
volume_size=VOLUME_SIZE,
364+
aggregate_name=AGGREGATE_NAME,
365+
)
366+
367+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
368+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
369+
@patch("builtins.open")
370+
def test_handle_project_updated_svm_tag_removed(
371+
self,
372+
mock_open,
373+
mock_tags,
374+
mock_netapp_class,
375+
mock_conn,
376+
mock_nautobot,
377+
valid_update_event_data,
378+
):
379+
"""Test project update when SVM_UNDERSTACK tag is removed."""
380+
# Project no longer has SVM tag
381+
mock_tags.return_value = ["tag1", "tag2"]
382+
383+
mock_netapp_manager = MagicMock()
384+
mock_netapp_manager.check_if_svm_exists.return_value = (
385+
True # SVM exists but tag removed
386+
)
387+
mock_netapp_class.return_value = mock_netapp_manager
388+
389+
result = handle_project_updated(
390+
mock_conn, mock_nautobot, valid_update_event_data
391+
)
392+
393+
assert result == 0
394+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
395+
mock_netapp_manager.check_if_svm_exists.assert_called_once_with(
396+
project_id="test-project-123"
397+
)
398+
# Should not create SVM or volume when tag is removed
399+
mock_netapp_manager.create_svm.assert_not_called()
400+
mock_netapp_manager.create_volume.assert_not_called()
401+
402+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
403+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
404+
@patch("builtins.open")
405+
def test_handle_project_updated_random_tag_added(
406+
self,
407+
mock_open,
408+
mock_tags,
409+
mock_netapp_class,
410+
mock_conn,
411+
mock_nautobot,
412+
valid_update_event_data,
413+
):
414+
"""Test project update when random_text tag is added (no SVM tag)."""
415+
# Project has random tag but not SVM tag
416+
mock_tags.return_value = ["tag1", "random_text", "tag2"]
417+
418+
mock_netapp_manager = MagicMock()
419+
mock_netapp_manager.check_if_svm_exists.return_value = False
420+
mock_netapp_class.return_value = mock_netapp_manager
421+
422+
result = handle_project_updated(
423+
mock_conn, mock_nautobot, valid_update_event_data
424+
)
425+
426+
assert result == 0
427+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
428+
mock_netapp_manager.check_if_svm_exists.assert_called_once_with(
429+
project_id="test-project-123"
430+
)
431+
# Should not create SVM or volume when no SVM tag
432+
mock_netapp_manager.create_svm.assert_not_called()
433+
mock_netapp_manager.create_volume.assert_not_called()
434+
435+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
436+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
437+
@patch("builtins.open")
438+
def test_handle_project_updated_svm_exists_and_tag_exists(
439+
self,
440+
mock_open,
441+
mock_tags,
442+
mock_netapp_class,
443+
mock_conn,
444+
mock_nautobot,
445+
valid_update_event_data,
446+
):
447+
"""Test project update when SVM tag exists and SVM already exists."""
448+
# Project has SVM tag and SVM already exists
449+
mock_tags.return_value = [SVM_PROJECT_TAG, "tag1"]
450+
451+
mock_netapp_manager = MagicMock()
452+
mock_netapp_manager.check_if_svm_exists.return_value = True
453+
mock_netapp_class.return_value = mock_netapp_manager
454+
455+
result = handle_project_updated(
456+
mock_conn, mock_nautobot, valid_update_event_data
457+
)
458+
459+
assert result == 0
460+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
461+
mock_netapp_manager.check_if_svm_exists.assert_called_once_with(
462+
project_id="test-project-123"
463+
)
464+
# Should not create SVM or volume when both exist
465+
mock_netapp_manager.create_svm.assert_not_called()
466+
mock_netapp_manager.create_volume.assert_not_called()
467+
468+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
469+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
470+
@patch("builtins.open")
471+
def test_handle_project_updated_netapp_manager_failure(
472+
self,
473+
mock_open,
474+
mock_tags,
475+
mock_netapp_class,
476+
mock_conn,
477+
mock_nautobot,
478+
valid_update_event_data,
479+
):
480+
"""Test handling when NetAppManager creation fails during update."""
481+
mock_tags.return_value = [SVM_PROJECT_TAG]
482+
mock_netapp_class.side_effect = Exception("NetApp connection failed")
483+
484+
with pytest.raises(Exception, match="NetApp connection failed"):
485+
handle_project_updated(mock_conn, mock_nautobot, valid_update_event_data)
486+
487+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
488+
mock_netapp_class.assert_called_once()
489+
490+
@patch("understack_workflows.oslo_event.keystone_project.NetAppManager")
491+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
492+
@patch("builtins.open")
493+
def test_handle_project_updated_svm_creation_failure(
494+
self,
495+
mock_open,
496+
mock_tags,
497+
mock_netapp_class,
498+
mock_conn,
499+
mock_nautobot,
500+
valid_update_event_data,
501+
):
502+
"""Test handling when SVM creation fails during update."""
503+
mock_tags.return_value = [SVM_PROJECT_TAG]
504+
505+
mock_netapp_manager = MagicMock()
506+
mock_netapp_manager.check_if_svm_exists.return_value = False
507+
mock_netapp_manager.create_svm.side_effect = Exception("SVM creation failed")
508+
mock_netapp_class.return_value = mock_netapp_manager
509+
510+
with pytest.raises(Exception, match="SVM creation failed"):
511+
handle_project_updated(mock_conn, mock_nautobot, valid_update_event_data)
512+
513+
mock_tags.assert_called_once_with(mock_conn, "test-project-123")
514+
mock_netapp_manager.check_if_svm_exists.assert_called_once_with(
515+
project_id="test-project-123"
516+
)
517+
mock_netapp_manager.create_svm.assert_called_once_with(
518+
project_id="test-project-123", aggregate_name=AGGREGATE_NAME
519+
)
520+
521+
def test_handle_project_updated_invalid_event_data(self, mock_conn, mock_nautobot):
522+
"""Test handling update with invalid event data."""
523+
invalid_event_data = {
524+
"event_type": "identity.project.updated",
525+
"payload": {}, # Missing target
526+
}
527+
528+
with pytest.raises(Exception, match="no target information in payload"):
529+
handle_project_updated(mock_conn, mock_nautobot, invalid_event_data)
530+
531+
@patch("understack_workflows.oslo_event.keystone_project._keystone_project_tags")
532+
@patch("builtins.open")
533+
def test_handle_project_updated_output_files_written(
534+
self, mock_open, mock_tags, mock_conn, mock_nautobot, valid_update_event_data
535+
):
536+
"""Test that output files are written correctly during update."""
537+
mock_tags.return_value = ["random_tag"]
538+
539+
with patch(
540+
"understack_workflows.oslo_event.keystone_project.NetAppManager"
541+
) as mock_netapp_class:
542+
mock_netapp_manager = MagicMock()
543+
mock_netapp_manager.check_if_svm_exists.return_value = False
544+
mock_netapp_class.return_value = mock_netapp_manager
545+
546+
result = handle_project_updated(
547+
mock_conn, mock_nautobot, valid_update_event_data
548+
)
549+
550+
assert result == 0
551+
# Verify output files are written
552+
expected_calls = [
553+
call("/var/run/argo/output.svm_enabled", "w"),
554+
call("/var/run/argo/output.svm_name", "w"),
555+
]
556+
mock_open.assert_has_calls(expected_calls, any_order=True)

python/understack-workflows/understack_workflows/main/openstack_oslo_event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class NoEventHandlerError(Exception):
6565
"baremetal.port.update.end": ironic_port.handle_port_create_update,
6666
"baremetal.port.delete.end": ironic_port.handle_port_delete,
6767
"identity.project.created": keystone_project.handle_project_created,
68+
"identity.project.updated": keystone_project.handle_project_updated,
6869
}
6970

7071

python/understack-workflows/understack_workflows/netapp_manager.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ def create_volume(
105105
logger.error("Error creating Volume: %s", e)
106106
exit(1)
107107

108+
def check_if_svm_exists(self, project_id):
109+
svm_name = self._svm_name(project_id)
110+
111+
try:
112+
if Svm.find(name=svm_name):
113+
return True
114+
except NetAppRestError:
115+
return False
116+
108117
def _svm_name(self, project_id):
109118
return f"os-{project_id}"
110119

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

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,64 @@ def handle_project_created(
6666
svm_name = None
6767
try:
6868
netapp_manager = NetAppManager()
69-
svm_name = netapp_manager.create_svm(
70-
project_id=event.project_id, aggregate_name=AGGREGATE_NAME
71-
)
72-
netapp_manager.create_volume(
73-
project_id=event.project_id,
74-
volume_size=VOLUME_SIZE,
75-
aggregate_name=AGGREGATE_NAME,
76-
)
69+
svm_name = _create_svm_and_volume(netapp_manager, event)
7770
finally:
7871
if not svm_name:
7972
svm_name = "not_returned"
8073
_save_output("svm_name", svm_name)
8174
return 0
8275

8376

77+
def handle_project_updated(
78+
conn: Connection, _nautobot: Nautobot, event_data: dict
79+
) -> int:
80+
if event_data.get("event_type") != "identity.project.updated":
81+
logger.error("Received event that is not identity.project.updated")
82+
return 1
83+
84+
event = KeystoneProjectEvent.from_event_dict(event_data)
85+
logger.info("Starting ONTAP SVM and Volume create/update workflow.")
86+
tags = _keystone_project_tags(conn, event.project_id)
87+
logger.debug("Project %s has tags: %s", event.project_id, tags)
88+
89+
project_is_svm_enabled = SVM_PROJECT_TAG in tags
90+
_save_output("svm_enabled", str(project_is_svm_enabled))
91+
92+
svm_name = None
93+
try:
94+
netapp_manager = NetAppManager()
95+
svm_exists = netapp_manager.check_if_svm_exists(project_id=event.project_id)
96+
97+
# Tag removed
98+
if not project_is_svm_enabled and svm_exists:
99+
logger.error(
100+
"SVM os-%s exists on NetApp but project %s is no longer tagged with %s",
101+
event.project_id,
102+
event.project_id,
103+
SVM_PROJECT_TAG,
104+
)
105+
# Tag added
106+
elif project_is_svm_enabled and not svm_exists:
107+
svm_name = _create_svm_and_volume(netapp_manager, event)
108+
finally:
109+
if not svm_name:
110+
svm_name = "not_returned"
111+
_save_output("svm_name", svm_name)
112+
return 0
113+
114+
115+
def _create_svm_and_volume(netapp_manager, event) -> str:
116+
svm_name = netapp_manager.create_svm(
117+
project_id=event.project_id, aggregate_name=AGGREGATE_NAME
118+
)
119+
netapp_manager.create_volume(
120+
project_id=event.project_id,
121+
volume_size=VOLUME_SIZE,
122+
aggregate_name=AGGREGATE_NAME,
123+
)
124+
return svm_name
125+
126+
84127
def _save_output(name, value):
85128
with open(f"{OUTPUT_BASE_PATH}/output.{name}", "w") as f:
86129
return f.write(value)

workflows/openstack/sensors/sensor-keystone-oslo-event.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ metadata:
99
Triggers on the following Keystone Events:
1010
1111
- identity.project.created
12+
- identity.project.updated
1213
- other events are silently ignored now
1314
1415
Resulting code should be very similar to:
@@ -37,6 +38,7 @@ spec:
3738
type: "string"
3839
value:
3940
- "identity.project.created"
41+
- "identity.project.updated"
4042
template:
4143
serviceAccountName: sensor-submit-workflow
4244
triggers:

0 commit comments

Comments
 (0)