|
14 | 14 | """ |
15 | 15 |
|
16 | 16 | import argparse |
| 17 | +from functools import partial |
17 | 18 | import getpass |
18 | 19 | import logging |
19 | 20 | import os |
|
28 | 29 | DEFAULT_PREFIX = "scs-test-" |
29 | 30 |
|
30 | 31 |
|
31 | | -def wait_for_resource( |
| 32 | +def check_resources( |
| 33 | + get_func: typing.Callable[[], [openstack.resource.Resource]], |
| 34 | + prefix: str, |
| 35 | +) -> None: |
| 36 | + remaining = [b for b in get_func() if b.name.startswith(prefix)] |
| 37 | + if remaining: |
| 38 | + raise RuntimeError(f"unexpected resources: {remaining}") |
| 39 | + |
| 40 | + |
| 41 | +def check_resource( |
32 | 42 | get_func: typing.Callable[[str], openstack.resource.Resource], |
33 | 43 | resource_id: str, |
34 | 44 | expected_status=("available", ), |
| 45 | +) -> None: |
| 46 | + resource = get_func(resource_id) |
| 47 | + if resource is None: |
| 48 | + raise RuntimeError(f"resource {resource_id} not found") |
| 49 | + if resource.status not in expected_status: |
| 50 | + raise RuntimeError( |
| 51 | + f"Expect resource {resource_id} in " |
| 52 | + f"to be in status {expected_status} (current: {resource.status})" |
| 53 | + ) |
| 54 | + |
| 55 | + |
| 56 | +class TimeoutError(Exception): |
| 57 | + pass |
| 58 | + |
| 59 | + |
| 60 | +def retry( |
| 61 | + func: callable, |
35 | 62 | timeouts=(2, 3, 5, 10, 15, 25, 50), |
36 | 63 | ) -> None: |
37 | 64 | seconds_waited = 0 |
38 | 65 | timeout_iter = iter(timeouts) |
39 | | - resource = get_func(resource_id) |
40 | | - while resource is None or resource.status not in expected_status: |
41 | | - wait_delay = next(timeout_iter, None) |
42 | | - if wait_delay is None: |
43 | | - raise RuntimeError( |
44 | | - f"Timed out after {seconds_waited} s: waiting for resource {resource_id} " |
45 | | - f"to be in status {expected_status} (current: {resource and resource.status})" |
46 | | - ) |
47 | | - time.sleep(wait_delay) |
48 | | - seconds_waited += wait_delay |
49 | | - resource = get_func(resource_id) |
| 66 | + while True: |
| 67 | + try: |
| 68 | + func() |
| 69 | + except Exception as e: |
| 70 | + wait_delay = next(timeout_iter, None) |
| 71 | + if wait_delay is None: |
| 72 | + raise TimeoutError(f"Timed out after {seconds_waited} s: {e!s}") |
| 73 | + time.sleep(wait_delay) |
| 74 | + seconds_waited += wait_delay |
| 75 | + else: |
| 76 | + break |
| 77 | + |
| 78 | + |
| 79 | +def wait_for_resource( |
| 80 | + get_func: typing.Callable[[str], openstack.resource.Resource], |
| 81 | + resource_id: str, |
| 82 | + expected_status=("available", ), |
| 83 | +) -> None: |
| 84 | + retry(partial(check_resource, get_func, resource_id, expected_status)) |
| 85 | + |
| 86 | + |
| 87 | +def wait_for_resources( |
| 88 | + get_func: typing.Callable[[], [openstack.resource.Resource]], |
| 89 | + prefix: str, |
| 90 | +): |
| 91 | + retry(partial(check_resources, get_func, prefix)) |
50 | 92 |
|
51 | 93 |
|
52 | 94 | def test_backup(conn: openstack.connection.Connection, prefix=DEFAULT_PREFIX) -> None: |
@@ -131,33 +173,25 @@ def cleanup(conn: openstack.connection.Connection, prefix=DEFAULT_PREFIX) -> boo |
131 | 173 | expected_status=("available", "error"), |
132 | 174 | ) |
133 | 175 | logging.info(f"↳ deleting volume backup '{backup.id}' ...") |
134 | | - conn.block_storage.delete_backup(backup.id) |
135 | | - except openstack.exceptions.ResourceNotFound: |
136 | | - # if the resource has vanished on its own in the meantime ignore it |
137 | | - continue |
| 176 | + conn.block_storage.delete_backup(backup.id, ignore_missing=False) |
138 | 177 | except Exception as e: |
| 178 | + if isinstance(e, openstack.exceptions.ResourceNotFound): |
| 179 | + # if the resource has vanished on its own in the meantime ignore it |
| 180 | + # however, ResourceNotFound will also be thrown if the service 'cinder-backup' is missing |
| 181 | + if 'cinder-backup' in str(e): |
| 182 | + raise |
| 183 | + continue |
139 | 184 | # Most common exception would be a timeout in wait_for_resource. |
140 | 185 | # We do not need to increment cleanup_issues here since |
141 | 186 | # any remaining ones will be caught in the next loop down below anyway. |
142 | | - logging.debug("traceback", exc_info=True) |
143 | 187 | logging.warning(str(e)) |
144 | 188 |
|
145 | 189 | # wait for all backups to be cleaned up before attempting to remove volumes |
146 | | - seconds_waited = 0 |
147 | | - while len( |
148 | | - # list of all backups whose name starts with the prefix |
149 | | - [b for b in conn.block_storage.backups() if b.name.startswith(prefix)] |
150 | | - ) > 0: |
151 | | - time.sleep(1.0) |
152 | | - seconds_waited += 1 |
153 | | - if seconds_waited >= 100: |
154 | | - cleanup_issues += 1 |
155 | | - logging.warning( |
156 | | - f"Timeout reached while waiting for all backups with prefix " |
157 | | - f"'{prefix}' to finish deletion during cleanup after " |
158 | | - f"{seconds_waited} seconds" |
159 | | - ) |
160 | | - break |
| 190 | + try: |
| 191 | + wait_for_resources(conn.block_storage.backups, prefix) |
| 192 | + except TimeoutError as e: |
| 193 | + cleanup_issues += 1 |
| 194 | + logging.warning(str(e)) |
161 | 195 |
|
162 | 196 | volumes = conn.block_storage.volumes() |
163 | 197 | for volume in volumes: |
|
0 commit comments