From d4489c52e4805b52ecda3a48756e435cdd3769c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Wed, 28 Jan 2026 21:51:49 +0100 Subject: [PATCH 1/6] Implement output of testcase-specific report messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- Tests/iaas/openstack_test.py | 34 +++++++++++++++++-- .../scs_0102_image_metadata/image_metadata.py | 9 ++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Tests/iaas/openstack_test.py b/Tests/iaas/openstack_test.py index 2cccff674..9e68ddcf8 100755 --- a/Tests/iaas/openstack_test.py +++ b/Tests/iaas/openstack_test.py @@ -232,7 +232,8 @@ def __init__(self): def __getattr__(self, key): val = self._values.get(key) if val is None: - logger.debug(f'... {key}') + # this is too verbose + # logger.debug(f'... {key}') try: ret = self._functions[key](self) except BaseException as e: @@ -256,6 +257,27 @@ def add_value(self, name, value): self._values[name] = value +def _eval_result(result, messages): + """evaluates `result` from calling a check function + + returns 0 if check function succeeded, otherwise 1; + appends to list `messages` if the result contains messages + """ + # either a pair (success, messages) + if isinstance(result, tuple): + success, msgs = result + messages.extend(messages) + return not success + # or just a list of messages + if isinstance(result, list): + if result: + messages.extend(result) + return 1 + return 0 + # or just a scalar (no messages) + return not result + + def harness(name, *check_fns): """Harness for evaluating testcase `name`. @@ -268,15 +290,21 @@ def harness(name, *check_fns): - 'PASS' otherwise """ logger.debug(f'** {name}') + messages = [] try: - result = all(check_fn() for check_fn in check_fns) + results = [check_fn() for check_fn in check_fns] except BaseException: logger.debug('exception during check', exc_info=True) result = 'ABORT' else: - result = ['FAIL', 'PASS'][min(1, result)] + fails = 0 + for r in results: + fails += _eval_result(r, messages) + result = ['FAIL', 'PASS'][fails == 0] # this is quite redundant # logger.debug(f'** computation end for {name}') + for msg in messages: + print(f"{name}: {msg}") print(f"{name}: {result}") diff --git a/Tests/iaas/scs_0102_image_metadata/image_metadata.py b/Tests/iaas/scs_0102_image_metadata/image_metadata.py index a22501241..03f4b4b2d 100644 --- a/Tests/iaas/scs_0102_image_metadata/image_metadata.py +++ b/Tests/iaas/scs_0102_image_metadata/image_metadata.py @@ -101,9 +101,11 @@ def is_outdated(img, now=time.time()): def _log_error(cause, offenders, channel=logging.ERROR): if not offenders: - return + return [] names = [img.name for img in offenders] - logger.log(channel, f"{cause} for image(s): {', '.join(names)}") + message = f"{cause} for image(s): {', '.join(names)}" + logger.log(channel, message) + return [message] def compute_scs_0102_prop_architecture(images, architectures=ARCHITECTURES): @@ -165,8 +167,7 @@ def compute_scs_0102_prop_os_distro(images): def compute_scs_0102_prop_os_purpose(images, os_purposes=OS_PURPOSES): """This test ensures that each image has a proper value for the property `os_distro`.""" offenders = [img for img in images if img.properties.get('os_purpose') not in os_purposes] - _log_error('property os_purpose not set or not correct', offenders) - return not offenders + return _log_error('property os_purpose not set or not correct', offenders) def compute_scs_0102_prop_hw_disk_bus(images, hw_disk_buses=HW_DISK_BUSES): From b79b074cb936c82401a475db239f7cade3767bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Fri, 30 Jan 2026 14:49:39 +0100 Subject: [PATCH 2/6] Upgrade more testcases to output messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- Tests/iaas/openstack_test.py | 4 +- .../flavor_names_check.py | 13 ++- .../scs_0102_image_metadata/image_metadata.py | 91 +++++++++---------- .../standard_images.py | 13 ++- 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/Tests/iaas/openstack_test.py b/Tests/iaas/openstack_test.py index 9e68ddcf8..245842b9c 100755 --- a/Tests/iaas/openstack_test.py +++ b/Tests/iaas/openstack_test.py @@ -232,8 +232,8 @@ def __init__(self): def __getattr__(self, key): val = self._values.get(key) if val is None: - # this is too verbose - # logger.debug(f'... {key}') + # I thought this was too verbose, but it massively helps classifying log messages + logger.debug(f'... {key}') try: ret = self._functions[key](self) except BaseException as e: diff --git a/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py b/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py index 9bbab493b..84c42a11b 100644 --- a/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py +++ b/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py @@ -38,12 +38,19 @@ def compute_scs_flavors(flavors: typing.List[openstack.compute.v2.flavor.Flavor] return result +def _log_errors(cause, names): + """helper to construct result for testcase""" + if not names: + return [] + message = f"{cause} with flavor(s): {', '.join(sorted(names))}" + logger.error(message) + return message + + def compute_scs_0100_syntax_check(scs_flavors: list) -> bool: """This test ensures that each SCS flavor is indeed named correctly.""" problems = [flv.name for flv, flavorname in scs_flavors if not flavorname] - if problems: - logger.error(f"scs-100-syntax-check: flavor(s) failed: {', '.join(sorted(problems))}") - return not problems + return _log_errors('syntax problems', problems) def compute_scs_0100_semantics_check(scs_flavors: list) -> bool: diff --git a/Tests/iaas/scs_0102_image_metadata/image_metadata.py b/Tests/iaas/scs_0102_image_metadata/image_metadata.py index 03f4b4b2d..3c04dacea 100644 --- a/Tests/iaas/scs_0102_image_metadata/image_metadata.py +++ b/Tests/iaas/scs_0102_image_metadata/image_metadata.py @@ -100,6 +100,7 @@ def is_outdated(img, now=time.time()): def _log_error(cause, offenders, channel=logging.ERROR): + """helper function to construct report messages""" if not offenders: return [] names = [img.name for img in offenders] @@ -111,36 +112,34 @@ def _log_error(cause, offenders, channel=logging.ERROR): def compute_scs_0102_prop_architecture(images, architectures=ARCHITECTURES): """This test ensures that each image has a proper value for the property `architecture`.""" offenders = [img for img in images if img.architecture not in architectures] - _log_error('property architecture not correct', offenders) - return not offenders + return _log_error('property architecture not correct', offenders) # NOTE I think this is a recommendation def compute_scs_0102_prop_hash_algo(images): """This test ensures that each image has a proper value for the property `hash_algo`.""" offenders = [img for img in images if img.hash_algo not in ('sha256', 'sha512')] - _log_error('property hash_algo invalid', offenders) - return not offenders + return _log_error('property hash_algo invalid', offenders) def compute_scs_0102_prop_min_disk(images): """This test ensures that each image has a proper value for the property `min_disk`.""" offenders1 = [img for img in images if not img.min_disk] - _log_error('property min_disk not set', offenders1) offenders2 = [img for img in images if img.min_disk and img.min_disk * GIB < img.size] - _log_error('property min_disk smaller than size', offenders2) - return not offenders1 and not offenders2 + return ( + _log_error('property min_disk not set', offenders1) + + _log_error('property min_disk smaller than size', offenders2) + ) def compute_scs_0102_prop_min_ram(images): """This test ensures that each image has a proper value for the property `min_ram`.""" - offenders1 = [img for img in images if not img.min_ram] - _log_error('property min_ram not set', offenders1) # emit a warning im RAM really low # NOTE this will probably only get noticed if an error occurs as well offenders2 = [img for img in images if img.min_ram and img.min_ram < 64] _log_error('property min_ram < 64 MiB', offenders2, channel=logging.WARNING) - return not offenders1 + offenders1 = [img for img in images if not img.min_ram] + return _log_error('property min_ram not set', offenders1) def compute_scs_0102_prop_os_version(images): @@ -150,8 +149,7 @@ def compute_scs_0102_prop_os_version(images): # certain values for common operating systems. # - os_version not matching regexp r'[0-9\.]*' (should be a numeric version no) offenders = [img for img in images if not img.os_version] - _log_error('property os_version not set', offenders) - return not offenders + return _log_error('property os_version not set', offenders) def compute_scs_0102_prop_os_distro(images): @@ -160,8 +158,7 @@ def compute_scs_0102_prop_os_distro(images): # - os_distro not being all-lowercase (they all should be acc. to # https://docs.openstack.org/glance/2025.1/admin/useful-image-properties.html offenders = [img for img in images if not img.os_distro] - _log_error('property os_distro not set', offenders) - return not offenders + return _log_error('property os_distro not set', offenders) def compute_scs_0102_prop_os_purpose(images, os_purposes=OS_PURPOSES): @@ -173,41 +170,44 @@ def compute_scs_0102_prop_os_purpose(images, os_purposes=OS_PURPOSES): def compute_scs_0102_prop_hw_disk_bus(images, hw_disk_buses=HW_DISK_BUSES): """This test ensures that each image has a proper value for the property `hw_disk_bus`.""" offenders = [img for img in images if img.hw_disk_bus not in hw_disk_buses] - _log_error('property hw_disk_bus not correct', offenders) - return not offenders + return _log_error('property hw_disk_bus not correct', offenders) def compute_scs_0102_prop_hypervisor_type(images, hypervisor_types=HYPERVISOR_TYPES): """This test ensures that each image has a proper value for the property `hypervisor_type`.""" offenders = [img for img in images if img.hypervisor_type not in hypervisor_types] - _log_error('property hypervisor_type not correct', offenders) - return not offenders + return _log_error('property hypervisor_type not correct', offenders) def compute_scs_0102_prop_hw_rng_model(images, hw_rng_models=HW_RNG_MODELS): """This test ensures that each image has a proper value for the property `hw_rng_model`.""" offenders = [img for img in images if img.hw_rng_model not in hw_rng_models] - _log_error('property hw_rng_model not correct', offenders) - return not offenders + return _log_error('property hw_rng_model not correct', offenders) def compute_scs_0102_prop_image_build_date(images, now=time.time()): """This test ensures that each image has a proper value for the property `image_build_date`.""" - errors = 0 + offenders1 = [] + offenders2 = [] + offenders3 = [] for img in images: rdate = parse_date(img.created_at, formats=STRICT_FORMATS) bdate_str = img.properties.get('image_build_date', '') bdate = parse_date(bdate_str) if not bdate: logger.error(f'Image "{img.name}": image_build_date "{bdate_str}" INVALID') - errors += 1 + offenders1.append(img) elif bdate > rdate: logger.error(f'Image "{img.name}": image_build_date {bdate_str} AFTER registration date {img.created_at}') - errors += 1 + offenders3.append(img) if (bdate or rdate) > now: logger.error(f'Image "{img.name}" has build time in the future: {bdate}') - errors += 1 - return not errors + offenders3.append(img) + return ( + _log_error('image_build_date INVALID', offenders1, channel=logging.DEBUG) + + _log_error('image_build_date AFTER registration date', offenders2, channel=logging.DEBUG) + + _log_error('image build time in the future', offenders3, channel=logging.DEBUG) + ) # FIXME this is completely optional @@ -218,8 +218,7 @@ def compute_scs_0102_prop_image_build_date(images, now=time.time()): def compute_scs_0102_prop_image_original_user(images): """This test ensures that each image has a proper value for the property `image_original_user`.""" offenders = [img for img in images if not img.properties.get('image_original_user')] - _log_error('property image_original_user not set', offenders) - return not offenders + return _log_error('property image_original_user not set', offenders) def compute_scs_0102_prop_image_source(images): @@ -230,34 +229,30 @@ def compute_scs_0102_prop_image_source(images): if img.properties.get('image_source') != 'private' if not is_url(img.properties.get('image_source', '')) ] - _log_error('property image_source INVALID (url or "private")', offenders) - return not offenders + return _log_error('property image_source INVALID (url or "private")', offenders) def compute_scs_0102_prop_image_description(images): """This test ensures that each image has a proper value for the property `image_description`.""" offenders = [img for img in images if not img.properties.get('image_description')] - _log_error('property image_description not set', offenders) - return not offenders + return _log_error('property image_description not set', offenders) def compute_scs_0102_prop_replace_frequency(images, replace_frequencies=FREQ_TO_SEC): """This test ensures that each image has a proper value for the property `replace_frequency`.""" offenders = [img for img in images if img.properties.get('replace_frequency') not in replace_frequencies] - _log_error('property replace_frequency not correct', offenders) - return not offenders + return _log_error('property replace_frequency not correct', offenders) def compute_scs_0102_prop_provided_until(images): """This test ensures that each image has a proper value for the property `provided_until`.""" offenders = [img for img in images if not img.properties.get('provided_until')] - _log_error('property provided_until not set', offenders) - return not offenders + return _log_error('property provided_until not set', offenders) def compute_scs_0102_prop_uuid_validity(images): """This test ensures that each image has a proper value for the property `uuid_validity`.""" - errors = 0 + offenders = [] for img in images: img_uuid_val = img.properties.get("uuid_validity") if img_uuid_val in (None, "none", "notice", "forever"): @@ -268,20 +263,20 @@ def compute_scs_0102_prop_uuid_validity(images): pass else: logger.error(f'Image "{img.name}": property uuid_validity INVALID: {img_uuid_val}') - errors += 1 - return not errors + offenders.append(img) + return _log_error('uuid_validity INVALID', offenders, channel=logging.DEBUG) def compute_scs_0102_prop_hotfix_hours(images): """This test ensures that each image has a proper value for the property `hotfix_hours`.""" - errors = 0 + offenders = [] for img in images: hotfix_hours = img.properties.get("hotfix_hours", '') if not hotfix_hours or hotfix_hours.isdecimal(): continue logger.error(f'Image "{img.name}": property hotfix_hours INVALID: {hotfix_hours}') - errors += 1 - return not errors + offenders.append(img) + return _log_error('hotfix_hours INVALID', offenders, channel=logging.DEBUG) def _find_replacement_image(by_name, img_name): @@ -310,7 +305,8 @@ def compute_scs_0102_image_recency(images): counter = Counter([img.name for img in images]) duplicates = [name for name, count in counter.items() if count > 1] logger.warning(f'duplicate names detected: {", ".join(duplicates)}') - errors = 0 + offenders1 = [] + offenders2 = [] for img in images: # This is a bit tricky: We need to disregard images that have been rotated out # - os_hidden = True is a safe sign for this @@ -319,8 +315,7 @@ def compute_scs_0102_image_recency(images): if not outd: continue # fine if outd == 3: - logger.error(f'Image "{img.name}" does not provide a valid provided until date') - errors += 1 + offenders1.append(img) continue # hopeless # in case that outd in (1, 2) try to find a non-outdated version if outd == 2: @@ -328,8 +323,10 @@ def compute_scs_0102_image_recency(images): # warnings += 1 replacement = _find_replacement_image(by_name, img.name) if replacement is None: - logger.error(f'Image "{img.name}" outdated without replacement') - errors += 1 + offenders2.append(img) else: logger.info(f'Image "{replacement.name}" is a valid replacement for outdated "{img.name}"') - return not errors + return ( + _log_error('images w/o valid provided_until', offenders1, channel=logging.DEBUG) + + _log_error('outdated images w/o replacement', offenders2, channel=logging.DEBUG) + ) diff --git a/Tests/iaas/scs_0104_standard_images/standard_images.py b/Tests/iaas/scs_0104_standard_images/standard_images.py index 6f8c827e8..749453a6f 100644 --- a/Tests/iaas/scs_0104_standard_images/standard_images.py +++ b/Tests/iaas/scs_0104_standard_images/standard_images.py @@ -70,16 +70,15 @@ def compute_scs_0104_source(image_lookup, image_spec): For an impression of what these specs look like, refer to `SCS_0104_IMAGE_SPECS`. """ matches = _lookup_images(image_lookup, image_spec) - errors = 0 + errors = [] for image in matches: img_source = image.properties.get('image_source', '') sources = image_spec['source'] if not isinstance(sources, (tuple, list)): sources = [sources] if not any(img_source.startswith(src) for src in sources): - errors += 1 - logger.error(f"Image '{image.name}' source mismatch: '{img_source}' matches none of these prefixes: {', '.join(sources)}") - return not errors + errors.append(f"Image '{image.name}' source mismatch: '{img_source}' matches none of these prefixes: {', '.join(sources)}") + return errors def compute_scs_0104_image(image_lookup, image_spec): @@ -89,7 +88,7 @@ def compute_scs_0104_image(image_lookup, image_spec): For an impression of what these specs look like, refer to `SCS_0104_IMAGE_SPECS`. """ matches = _lookup_images(image_lookup, image_spec) + errors = [] if not matches: - logger.error(f"Missing image '{image_spec['name']}'") - return False - return True + errors.append(f"Missing image '{image_spec['name']}'") + return errors From 71ae00884ced5bac786ba3799a8f3dd7e570444e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Fri, 30 Jan 2026 15:35:14 +0100 Subject: [PATCH 3/6] Fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- .../flavor_names_check.py | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py b/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py index 84c42a11b..bfce69d6d 100644 --- a/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py +++ b/Tests/iaas/scs_0100_flavor_naming/flavor_names_check.py @@ -9,7 +9,6 @@ logger = logging.getLogger(__name__) -TESTCASES = ('scs-0100-syntax-check', 'scs-0100-semantics-check', 'flavor-name-check') STRATEGY = flavor_names.ParsingStrategy( vstr='v3', parsers=(flavor_names.parser_v3, ), @@ -32,25 +31,14 @@ def compute_scs_flavors(flavors: typing.List[openstack.compute.v2.flavor.Flavor] try: flavorname = parser(flv.name) except ValueError as exc: - logger.info(f"error parsing {flv.name}: {exc}") - flavorname = None + flavorname = f"error parsing {flv.name}: {exc}" result.append((flv, flavorname)) return result -def _log_errors(cause, names): - """helper to construct result for testcase""" - if not names: - return [] - message = f"{cause} with flavor(s): {', '.join(sorted(names))}" - logger.error(message) - return message - - def compute_scs_0100_syntax_check(scs_flavors: list) -> bool: """This test ensures that each SCS flavor is indeed named correctly.""" - problems = [flv.name for flv, flavorname in scs_flavors if not flavorname] - return _log_errors('syntax problems', problems) + return [flavorname for _, flavorname in scs_flavors if isinstance(flavorname, str)] def compute_scs_0100_semantics_check(scs_flavors: list) -> bool: @@ -61,36 +49,31 @@ def compute_scs_0100_semantics_check(scs_flavors: list) -> bool: NOTE that this test is incomplete; it only checks the most obvious properties. See also . """ - problems = set() + problems = [] for flv, flavorname in scs_flavors: - if not flavorname: + if isinstance(flavorname, str): continue # this case is handled by syntax check cpuram = flavorname.cpuram if flv.vcpus < cpuram.cpus: - logger.error(f"Flavor {flv.name} CPU overpromise: {flv.vcpus} < {cpuram.cpus}") - problems.add(flv.name) + problems.append(f"CPU overpromise for {flv.name!r}: {flv.vcpus} < {cpuram.cpus}") elif flv.vcpus > cpuram.cpus: - logger.info(f"Flavor {flv.name} CPU underpromise: {flv.vcpus} > {cpuram.cpus}") + logger.info(f"CPU underpromise for {flv.name!r}: {flv.vcpus} > {cpuram.cpus}") # RAM flvram = int((flv.ram + 51) / 102.4) / 10 # Warn for strange sizes (want integer numbers, half allowed for < 10GiB) if flvram >= 10 and flvram != int(flvram) or flvram * 2 != int(flvram * 2): - logger.info(f"Flavor {flv.name} uses discouraged uneven size of memory {flvram:.1f} GiB") + logger.info(f"Discouraged uneven size of memory for {flv.name!r}: {flvram:.1f} GiB") if flvram < cpuram.ram: - logger.error(f"Flavor {flv.name} RAM overpromise {flvram:.1f} < {cpuram.ram:.1f}") - problems.add(flv.name) + problems.append(f"RAM overpromise for {flv.name!r}: {flvram:.1f} < {cpuram.ram:.1f}") elif flvram > cpuram.ram: - logger.info(f"Flavor {flv.name} RAM underpromise {flvram:.1f} > {cpuram.ram:.1f}") + logger.info(f"RAM underpromise for {flv.name!r}: {flvram:.1f} > {cpuram.ram:.1f}") # Disk could have been omitted disksize = flavorname.disk.disksize if flavorname.disk else 0 # We have a recommendation for disk size steps if disksize not in ACC_DISK: - logger.info(f"Flavor {flv.name} non-standard disk size {disksize}, should have (5, 10, 20, 50, 100, 200, ...)") + logger.info(f"Non-standard disk size for {flv.name!r}: {disksize} not in (5, 10, 20, 50, 100, 200, ...)") if flv.disk < disksize: - logger.error(f"Flavor {flv.name} disk overpromise {flv.disk} < {disksize}") - problems.add(flv.name) + problems.append(f"Disk overpromise for {flv.name!r}: {flv.disk} < {disksize}") elif flv.disk > disksize: - logger.info(f"Flavor {flv.name} disk underpromise {flv.disk} > {disksize}") - if problems: - logger.error(f"scs-100-semantics-check: flavor(s) failed: {', '.join(sorted(problems))}") - return not problems + logger.info(f"Disk underpromise for {flv.name!r}: {flv.disk} > {disksize}") + return problems From a9ee058b40b6680b53f4b0b4f68364d1068099e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Fri, 30 Jan 2026 16:16:04 +0100 Subject: [PATCH 4/6] Fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- .../scs_0102_image_metadata/image_metadata.py | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/Tests/iaas/scs_0102_image_metadata/image_metadata.py b/Tests/iaas/scs_0102_image_metadata/image_metadata.py index 3c04dacea..87f8efb04 100644 --- a/Tests/iaas/scs_0102_image_metadata/image_metadata.py +++ b/Tests/iaas/scs_0102_image_metadata/image_metadata.py @@ -124,12 +124,15 @@ def compute_scs_0102_prop_hash_algo(images): def compute_scs_0102_prop_min_disk(images): """This test ensures that each image has a proper value for the property `min_disk`.""" - offenders1 = [img for img in images if not img.min_disk] - offenders2 = [img for img in images if img.min_disk and img.min_disk * GIB < img.size] - return ( - _log_error('property min_disk not set', offenders1) + - _log_error('property min_disk smaller than size', offenders2) + msgs1 = _log_error( + 'property min_disk not set', + [img for img in images if not img.min_disk], ) + msgs2 = _log_error( + 'property min_disk smaller than size', + [img for img in images if img.min_disk and img.min_disk * GIB < img.size], + ) + return msgs1 + msgs2 def compute_scs_0102_prop_min_ram(images): @@ -187,27 +190,20 @@ def compute_scs_0102_prop_hw_rng_model(images, hw_rng_models=HW_RNG_MODELS): def compute_scs_0102_prop_image_build_date(images, now=time.time()): """This test ensures that each image has a proper value for the property `image_build_date`.""" - offenders1 = [] - offenders2 = [] - offenders3 = [] + problems = [] for img in images: rdate = parse_date(img.created_at, formats=STRICT_FORMATS) bdate_str = img.properties.get('image_build_date', '') bdate = parse_date(bdate_str) - if not bdate: - logger.error(f'Image "{img.name}": image_build_date "{bdate_str}" INVALID') - offenders1.append(img) + if not bdate_str: + problems.append(f'image_build_date NOT SET for {img.name!r}') + elif not bdate: + problems.append(f'image_build_date INVALID for {img.name!r}: {bdate_str!r}') elif bdate > rdate: - logger.error(f'Image "{img.name}": image_build_date {bdate_str} AFTER registration date {img.created_at}') - offenders3.append(img) + problems.append(f'image_build_date AFTER registration for {img.name!r}: {bdate_str} > {img.created_at}') if (bdate or rdate) > now: - logger.error(f'Image "{img.name}" has build time in the future: {bdate}') - offenders3.append(img) - return ( - _log_error('image_build_date INVALID', offenders1, channel=logging.DEBUG) + - _log_error('image_build_date AFTER registration date', offenders2, channel=logging.DEBUG) + - _log_error('image build time in the future', offenders3, channel=logging.DEBUG) - ) + problems.append(f'image_build_date in the future for {img.name!r}: {bdate}') + return problems # FIXME this is completely optional @@ -223,13 +219,17 @@ def compute_scs_0102_prop_image_original_user(images): def compute_scs_0102_prop_image_source(images): """This test ensures that each image has a proper value for the property `image_source`.""" - offenders = [ - img - for img in images - if img.properties.get('image_source') != 'private' - if not is_url(img.properties.get('image_source', '')) - ] - return _log_error('property image_source INVALID (url or "private")', offenders) + problems = [] + for img in images: + src = img.properties.get('image_source') + if not src: + problems.append(f"image_source MISSING for {img.name}") + continue + if src == 'private': + continue + if not is_url(src): + problems.append(f'image_source INVALID (url or "private") for {img.name}') + return problems def compute_scs_0102_prop_image_description(images): @@ -305,8 +305,7 @@ def compute_scs_0102_image_recency(images): counter = Counter([img.name for img in images]) duplicates = [name for name, count in counter.items() if count > 1] logger.warning(f'duplicate names detected: {", ".join(duplicates)}') - offenders1 = [] - offenders2 = [] + problems = [] for img in images: # This is a bit tricky: We need to disregard images that have been rotated out # - os_hidden = True is a safe sign for this @@ -315,18 +314,15 @@ def compute_scs_0102_image_recency(images): if not outd: continue # fine if outd == 3: - offenders1.append(img) + problems.append(f'property provided_until INVALID for {img.name!r}') continue # hopeless # in case that outd in (1, 2) try to find a non-outdated version if outd == 2: - logger.warning(f'Image "{img.name}" seems outdated (acc. to its repl freq) but is not hidden or otherwise marked') + logger.warning(f'Image {img.name!r} seems outdated (acc. to its repl freq) but is not hidden or otherwise marked') # warnings += 1 replacement = _find_replacement_image(by_name, img.name) if replacement is None: - offenders2.append(img) + problems.append(f'outdated image w/o replacement: {img.name!r}') else: - logger.info(f'Image "{replacement.name}" is a valid replacement for outdated "{img.name}"') - return ( - _log_error('images w/o valid provided_until', offenders1, channel=logging.DEBUG) + - _log_error('outdated images w/o replacement', offenders2, channel=logging.DEBUG) - ) + logger.info(f'Image {replacement.name!r} is a valid replacement for outdated {img.name!r}') + return problems From 94597f962d401424ff69673bfc7bf76fa8e147b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Fri, 30 Jan 2026 23:48:04 +0100 Subject: [PATCH 5/6] Instrument logger so lines can be connected to testcases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- Tests/iaas/openstack_test.py | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/Tests/iaas/openstack_test.py b/Tests/iaas/openstack_test.py index 245842b9c..3982a27bd 100755 --- a/Tests/iaas/openstack_test.py +++ b/Tests/iaas/openstack_test.py @@ -205,6 +205,35 @@ def make_container(cloud): return c +class _Filter: + _instance = None + + def __init__(self): + self.components = [] + + def __call__(self, record): + if len(self.components) == 1 or len(self.components) == 2 and self.components[0].replace('-', '_') == self.components[1]: + record.msg = f'{self.components[0]}: {record.msg}' + elif self.components: + record.msg = f'[{self.components[-1]}] {record.msg}' + return True + + @classmethod + def install(cls, logger): + if cls._instance: + return + cls._instance = cls() + logger.handlers[0].filters.append(cls._instance) + + @classmethod + def push(cls, name): + cls._instance.components.append(name) + + @classmethod + def pop(cls): + del cls._instance.components[-1] + + class Container: """ This class does lazy evaluation and memoization. You register any potential value either @@ -233,13 +262,16 @@ def __getattr__(self, key): val = self._values.get(key) if val is None: # I thought this was too verbose, but it massively helps classifying log messages - logger.debug(f'... {key}') + # logger.debug(f'... {key}') + _Filter.push(key) try: ret = self._functions[key](self) except BaseException as e: val = (True, e) else: val = (False, ret) + _Filter.pop() + # logger.debug(f'... /{key}') self._values[key] = val error, ret = val if error: @@ -290,6 +322,8 @@ def harness(name, *check_fns): - 'PASS' otherwise """ logger.debug(f'** {name}') + _Filter.push(name) + messages = [] try: results = [check_fn() for check_fn in check_fns] @@ -301,6 +335,8 @@ def harness(name, *check_fns): for r in results: fails += _eval_result(r, messages) result = ['FAIL', 'PASS'][fails == 0] + finally: + _Filter.pop() # this is quite redundant # logger.debug(f'** computation end for {name}') for msg in messages: @@ -325,6 +361,7 @@ def run_sanity_checks(container): def main(argv): # configure logging, disable verbose library logging logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) + _Filter.install(logging.getLogger()) openstack.enable_logging(debug=False) cloud = None From 638ff8b69641d7614db8c21e84bff05448bc2ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCchse?= Date: Sat, 31 Jan 2026 00:03:39 +0100 Subject: [PATCH 6/6] Pacify flake8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Büchse --- Tests/iaas/openstack_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/iaas/openstack_test.py b/Tests/iaas/openstack_test.py index 3982a27bd..518e405a9 100755 --- a/Tests/iaas/openstack_test.py +++ b/Tests/iaas/openstack_test.py @@ -323,7 +323,7 @@ def harness(name, *check_fns): """ logger.debug(f'** {name}') _Filter.push(name) - + messages = [] try: results = [check_fn() for check_fn in check_fns]