88import pytest
99from openstack import exceptions as openstack_exceptions
1010
11+ from openstack_sync .plugins .common import ConfigError
1112from openstack_sync .plugins .ironic .runbooks import client
1213from openstack_sync .plugins .ironic .runbooks import markers
1314from openstack_sync .plugins .ironic .runbooks import prune
@@ -43,7 +44,7 @@ def test_owned_runbook_absent_from_the_desired_set_is_deleted():
4344 _prune (fake , [_spec ("CUSTOM_KEEP" )])
4445
4546 assert sorted (fake .runbooks ) == ["CUSTOM_KEEP" ]
46- assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE" ]
47+ assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE-uuid " ]
4748
4849
4950def test_owned_runbook_on_the_second_page_is_deleted ():
@@ -63,7 +64,7 @@ def test_owned_runbook_on_the_second_page_is_deleted():
6364 {"detail" : "true" , "limit" : 1 , "marker" : "CUSTOM_KEEP-uuid" },
6465 {"detail" : "true" , "limit" : 1 , "marker" : "CUSTOM_GONE-uuid" },
6566 ]
66- assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE" ]
67+ assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE-uuid " ]
6768
6869
6970def test_runbook_the_operator_does_not_own_is_kept ():
@@ -111,7 +112,7 @@ def vanish(path: str, method: str, **kwargs: Any) -> Any:
111112 with mock .patch .object (fake , "request" , side_effect = vanish ):
112113 _prune (fake , [_spec ("CUSTOM_KEEP" )])
113114
114- assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE" ]
115+ assert fake .calls_for ("DELETE" ) == ["/runbooks/CUSTOM_GONE-uuid " ]
115116
116117
117118def test_a_conflict_leaves_the_runbook_in_place ():
@@ -128,6 +129,44 @@ def conflict(path: str, method: str, **kwargs: Any) -> Any:
128129 assert sorted (fake .runbooks ) == ["CUSTOM_GONE" ]
129130
130131
132+ # ---------------------------------------------------------------------------
133+ # Identifiers
134+ # ---------------------------------------------------------------------------
135+
136+
137+ def test_the_delete_is_addressed_to_the_uuid_not_the_name ():
138+ """Prune chose this runbook from a list snapshot taken earlier.
139+
140+ By the time the DELETE goes out the name may belong to a runbook another CR
141+ has just created or adopted. The UUID from the snapshot cannot be reassigned
142+ that way, so it is what the delete addresses.
143+ """
144+ fake = FakeBaremetal ([_owned ("CUSTOM_GONE" )])
145+ uuid = fake .uuid_of ("CUSTOM_GONE" )
146+
147+ _prune (fake , [_spec ("CUSTOM_KEEP" )])
148+
149+ assert fake .calls_for ("DELETE" ) == [f"/runbooks/{ uuid } " ]
150+ assert fake .runbooks == {}
151+
152+
153+ def test_a_runbook_with_no_uuid_fails_the_prune_instead_of_deleting ():
154+ """Better a failed prune than a DELETE to /runbooks/None."""
155+ book = _owned ("CUSTOM_GONE" )
156+ del book ["uuid" ]
157+ fake = FakeBaremetal ([book ])
158+
159+ with pytest .raises (ConfigError , match = "without a uuid" ):
160+ _prune (fake , [_spec ("CUSTOM_KEEP" )])
161+
162+ assert fake .calls_for ("DELETE" ) == []
163+
164+
165+ # ---------------------------------------------------------------------------
166+ # Failures
167+ # ---------------------------------------------------------------------------
168+
169+
131170def test_a_failure_other_than_conflict_or_not_found_stops_the_prune ():
132171 """The framework reports a failed prune as a non-zero exit."""
133172 fake = FakeBaremetal ([_owned ("CUSTOM_GONE" )])
0 commit comments