From 7db1af57d50dc443059e9f32e497b7a157f8aa6e Mon Sep 17 00:00:00 2001 From: Miroslav Vadkerti Date: Thu, 4 Jun 2026 19:58:07 +0200 Subject: [PATCH 1/3] Replace deprecated oraculum with `fedora-distro-aliases` The Fedora image URL resolver in `testcloud/distro_utils/fedora.py` mapped the `latest`/`rawhide`/`branched` aliases to concrete release numbers by querying oraculum (the Packager Dashboard) at `https://packager-dashboard.fedoraproject.org/api/v1/releases`. Oraculum is being deprecated, so this moves the lookup to the [`fedora-distro-aliases`](https://github.com/rpm-software-management/fedora-distro-aliases) library, which sources the same data from Bodhi (`https://bodhi.fedoraproject.org/releases/`). A new `get_fedora_releases()` helper returns a dict shaped like the old oraculum `releases["fedora"]` payload (`{"rawhide", "branched", "stable"}`), so the alias-normalization logic is unchanged. Release lookups are cached via the library's `Cache` (gated on `CACHE_IMAGES`, TTL `TRUST_DEADLINE`), and failures map to `TestcloudImageError` as before. `fedora-distro-aliases>=1.5` is pinned because the caching API (`Cache`, `BadCache`, the `cache=` argument) was introduced in 1.5. Assisted-by: Claude Code Signed-off-by: Miroslav Vadkerti --- requirements.txt | 1 + setup.py | 1 + test/test_fedora.py | 54 ++++++++++++++++++++++++++++++++ testcloud/distro_utils/fedora.py | 49 ++++++++++++++++++++++++----- 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 test/test_fedora.py diff --git a/requirements.txt b/requirements.txt index 9cb45f0..d8080bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ libvirt-python requests peewee +fedora-distro-aliases>=1.5 #guestfs - after it is on pypi ( https://bugzilla.redhat.com/show_bug.cgi?id=1075594 ) # Test suite requirements diff --git a/setup.py b/setup.py index 62c270d..2878068 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def run(self): "peewee", "requests", "packaging", + "fedora-distro-aliases>=1.5", ], extras_require={"image_resolve_caching": ["requests_cache>=1.2"]}, ) diff --git a/test/test_fedora.py b/test/test_fedora.py new file mode 100644 index 0000000..bbb4640 --- /dev/null +++ b/test/test_fedora.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright 2026, Red Hat, Inc. +# License: GPL-2.0+ +# See the LICENSE file for more details on Licensing + +"""Tests for Fedora release alias resolution via fedora-distro-aliases.""" + +from unittest.mock import patch + +from munch import Munch + +from testcloud.distro_utils import fedora + + +def _distro(version, version_number): + return Munch(version=version, version_number=version_number) + + +def _aliases(development, latest_stable): + return { + "fedora-development": development, + "fedora-latest-stable": latest_stable, + } + + +class TestGetFedoraReleases: + """``get_fedora_releases`` maps fedora-distro-aliases data to the + oraculum-shaped ``{"rawhide", "branched", "stable"}`` dict.""" + + def _run(self, aliases): + with patch.object(fedora.config_data, "CACHE_IMAGES", False): + with patch.object(fedora, "get_distro_aliases", return_value=aliases): + return fedora.get_fedora_releases() + + def test_without_branched(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[_distro("44", "44")], + ) + assert self._run(aliases) == {"rawhide": 45, "branched": None, "stable": 44} + + def test_with_branched(self): + aliases = _aliases( + development=[_distro("44", "44"), _distro("rawhide", "45")], + latest_stable=[_distro("43", "43")], + ) + assert self._run(aliases) == {"rawhide": 45, "branched": 44, "stable": 43} + + def test_without_stable(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[], + ) + assert self._run(aliases) == {"rawhide": 45, "branched": None, "stable": None} diff --git a/testcloud/distro_utils/fedora.py b/testcloud/distro_utils/fedora.py index c5e6b0e..addbf98 100644 --- a/testcloud/distro_utils/fedora.py +++ b/testcloud/distro_utils/fedora.py @@ -7,6 +7,9 @@ import re import requests +from fedora_distro_aliases import get_distro_aliases, Cache +from fedora_distro_aliases.cache import BadCache + from testcloud import config from testcloud import exceptions from testcloud.distro_utils.misc import get_requests_session @@ -15,6 +18,38 @@ config_data = config.get_config() +def get_fedora_releases() -> dict: + """ + Resolve current Fedora rawhide/branched/stable release numbers via Bodhi + (using the fedora-distro-aliases library). + + Returns a dict shaped like the old oraculum ``releases["fedora"]`` payload: + ``{"rawhide": int, "branched": int | None, "stable": int | None}``. + """ + cache = None + if config_data.CACHE_IMAGES: + cache = Cache( + path="{}/fedora_distro_aliases_cache.json".format(config_data.DATA_DIR), + ttl=config_data.TRUST_DEADLINE * 60 * 60 * 24, + ) + + aliases = get_distro_aliases(cache=cache) + + # The highest development release is Rawhide (its ``version`` is overridden to + # "rawhide" while ``version_number`` keeps the numeric value); any remaining + # development release is the branched one. + devel = aliases["fedora-development"] + rawhide = next(distro for distro in devel if distro.version == "rawhide") + branched = [distro for distro in devel if distro.version != "rawhide"] + stable = aliases["fedora-latest-stable"] + + return { + "rawhide": int(rawhide.version_number), + "branched": int(branched[-1].version_number) if branched else None, + "stable": int(stable[0].version_number) if stable else None, + } + + def _process_coreos_url(version: str, arch: str, platform: str) -> str: """ Returns an CoreOS url in either qemu or openstack format @@ -78,19 +113,19 @@ def get_fedora_image_url(version: str, arch: str) -> str: # get Fedora Cloud url try: - oraculum_releases = session.get("https://packager-dashboard.fedoraproject.org/api/v1/releases").json() - except (ConnectionError, IndexError, requests.exceptions.JSONDecodeError): - log.error("Couldn't fetch Fedora releases from oraculum...") + fedora_releases = get_fedora_releases() + except (requests.exceptions.RequestException, BadCache, StopIteration, KeyError): + log.error("Couldn't fetch Fedora releases...") raise exceptions.TestcloudImageError - if oraculum_releases["fedora"]["branched"] and version == str(oraculum_releases["fedora"]["branched"]): + if fedora_releases["branched"] and version == str(fedora_releases["branched"]): version = "branched" - if not oraculum_releases["fedora"]["branched"] and version == "branched": + if not fedora_releases["branched"] and version == "branched": log.warning("Branched release currently doesn't exist, using rawhide...") version = "rawhide" - if version == str(oraculum_releases["fedora"]["rawhide"]): + if version == str(fedora_releases["rawhide"]): version = "rawhide" if version == "qa-matrix": @@ -123,7 +158,7 @@ def get_fedora_image_url(version: str, arch: str) -> str: return str(url) if version == "latest": - version = str(oraculum_releases["fedora"]["stable"]) + version = str(fedora_releases["stable"]) try: releases = session.get("https://getfedora.org/releases.json").json() From c61373277f693c0d0bdb8da3b8e59294a99b0108 Mon Sep 17 00:00:00 2001 From: Miroslav Vadkerti Date: Fri, 26 Jun 2026 13:18:07 +0200 Subject: [PATCH 2/3] Address review: catch-all error handling and native alias resolution Use a catch-all `except Exception` around `get_fedora_releases()` since every failure maps to the same `TestcloudImageError` with a generic log message, so the specific error source is not preserved anyway (and the enumerated list missed cases such as `ValueError` from `int(...)`). Resolve rawhide/branched from the library's `fedora-development` alias by position instead of matching the magic `"rawhide"` string, relying on the documented sort order (highest is Rawhide, the one below is branched). Assisted-by: Claude Code Signed-off-by: Miroslav Vadkerti --- test/test_fedora.py | 55 +++++++++++++++++++--------- testcloud/distro_utils/fedora.py | 61 ++++++++++++++------------------ 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/test/test_fedora.py b/test/test_fedora.py index bbb4640..c678291 100644 --- a/test/test_fedora.py +++ b/test/test_fedora.py @@ -3,9 +3,7 @@ # License: GPL-2.0+ # See the LICENSE file for more details on Licensing -"""Tests for Fedora release alias resolution via fedora-distro-aliases.""" - -from unittest.mock import patch +"""Tests for Fedora version resolution via fedora-distro-aliases.""" from munch import Munch @@ -23,32 +21,55 @@ def _aliases(development, latest_stable): } -class TestGetFedoraReleases: - """``get_fedora_releases`` maps fedora-distro-aliases data to the - oraculum-shaped ``{"rawhide", "branched", "stable"}`` dict.""" - - def _run(self, aliases): - with patch.object(fedora.config_data, "CACHE_IMAGES", False): - with patch.object(fedora, "get_distro_aliases", return_value=aliases): - return fedora.get_fedora_releases() +class TestResolveFedoraVersion: + """``_resolve_fedora_version`` normalizes version strings using the + aliases returned by ``fedora-distro-aliases``.""" - def test_without_branched(self): + def test_numeric_rawhide(self): aliases = _aliases( development=[_distro("rawhide", "45")], latest_stable=[_distro("44", "44")], ) - assert self._run(aliases) == {"rawhide": 45, "branched": None, "stable": 44} + assert fedora._resolve_fedora_version("45", aliases) == "rawhide" - def test_with_branched(self): + def test_numeric_branched(self): aliases = _aliases( development=[_distro("44", "44"), _distro("rawhide", "45")], latest_stable=[_distro("43", "43")], ) - assert self._run(aliases) == {"rawhide": 45, "branched": 44, "stable": 43} + assert fedora._resolve_fedora_version("44", aliases) == "branched" + + def test_latest_resolves_to_stable(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[_distro("44", "44")], + ) + assert fedora._resolve_fedora_version("latest", aliases) == "44" + + def test_branched_falls_back_to_rawhide(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[_distro("44", "44")], + ) + assert fedora._resolve_fedora_version("branched", aliases) == "rawhide" + + def test_rawhide_passthrough(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[_distro("44", "44")], + ) + assert fedora._resolve_fedora_version("rawhide", aliases) == "rawhide" + + def test_numeric_passthrough(self): + aliases = _aliases( + development=[_distro("rawhide", "45")], + latest_stable=[_distro("44", "44")], + ) + assert fedora._resolve_fedora_version("43", aliases) == "43" - def test_without_stable(self): + def test_latest_without_stable(self): aliases = _aliases( development=[_distro("rawhide", "45")], latest_stable=[], ) - assert self._run(aliases) == {"rawhide": 45, "branched": None, "stable": None} + assert fedora._resolve_fedora_version("latest", aliases) == "latest" diff --git a/testcloud/distro_utils/fedora.py b/testcloud/distro_utils/fedora.py index addbf98..38167a6 100644 --- a/testcloud/distro_utils/fedora.py +++ b/testcloud/distro_utils/fedora.py @@ -8,7 +8,6 @@ import requests from fedora_distro_aliases import get_distro_aliases, Cache -from fedora_distro_aliases.cache import BadCache from testcloud import config from testcloud import exceptions @@ -18,36 +17,41 @@ config_data = config.get_config() -def get_fedora_releases() -> dict: - """ - Resolve current Fedora rawhide/branched/stable release numbers via Bodhi - (using the fedora-distro-aliases library). - - Returns a dict shaped like the old oraculum ``releases["fedora"]`` payload: - ``{"rawhide": int, "branched": int | None, "stable": int | None}``. - """ +def _get_fedora_aliases(): + """Fetch Fedora release aliases from Bodhi with optional caching.""" cache = None if config_data.CACHE_IMAGES: cache = Cache( path="{}/fedora_distro_aliases_cache.json".format(config_data.DATA_DIR), ttl=config_data.TRUST_DEADLINE * 60 * 60 * 24, ) + return get_distro_aliases(cache=cache) - aliases = get_distro_aliases(cache=cache) - # The highest development release is Rawhide (its ``version`` is overridden to - # "rawhide" while ``version_number`` keeps the numeric value); any remaining - # development release is the branched one. +def _resolve_fedora_version(version, aliases): + """ + Normalize a Fedora version string against live release data. + + Maps numeric versions that match a development release to their alias + (e.g. ``"45"`` -> ``"rawhide"``) and resolves ``"latest"`` to the + current stable release number. + """ devel = aliases["fedora-development"] - rawhide = next(distro for distro in devel if distro.version == "rawhide") - branched = [distro for distro in devel if distro.version != "rawhide"] stable = aliases["fedora-latest-stable"] - return { - "rawhide": int(rawhide.version_number), - "branched": int(branched[-1].version_number) if branched else None, - "stable": int(stable[0].version_number) if stable else None, - } + for distro in devel: + if version == str(distro.version_number): + version = "rawhide" if distro.version == "rawhide" else "branched" + break + + if version == "branched" and all(d.version == "rawhide" for d in devel): + log.warning("Branched release currently doesn't exist, using rawhide...") + version = "rawhide" + + if version == "latest" and stable: + version = str(stable[0].version_number) + + return version def _process_coreos_url(version: str, arch: str, platform: str) -> str: @@ -113,20 +117,12 @@ def get_fedora_image_url(version: str, arch: str) -> str: # get Fedora Cloud url try: - fedora_releases = get_fedora_releases() - except (requests.exceptions.RequestException, BadCache, StopIteration, KeyError): + aliases = _get_fedora_aliases() + except Exception: log.error("Couldn't fetch Fedora releases...") raise exceptions.TestcloudImageError - if fedora_releases["branched"] and version == str(fedora_releases["branched"]): - version = "branched" - - if not fedora_releases["branched"] and version == "branched": - log.warning("Branched release currently doesn't exist, using rawhide...") - version = "rawhide" - - if version == str(fedora_releases["rawhide"]): - version = "rawhide" + version = _resolve_fedora_version(version, aliases) if version == "qa-matrix": if arch != "x86_64": @@ -157,9 +153,6 @@ def get_fedora_image_url(version: str, arch: str) -> str: raise exceptions.TestcloudImageError return str(url) - if version == "latest": - version = str(fedora_releases["stable"]) - try: releases = session.get("https://getfedora.org/releases.json").json() except (ConnectionError, requests.exceptions.JSONDecodeError): From 8c597ee5920774bad9e4efd0fc3cb33786879886 Mon Sep 17 00:00:00 2001 From: Miroslav Vadkerti Date: Fri, 26 Jun 2026 20:43:06 +0200 Subject: [PATCH 3/3] Map version resolution failures to `TestcloudImageError` Move the `_resolve_fedora_version()` call inside the `try` block so that any failure while parsing the alias data (not just fetching it) is mapped to `TestcloudImageError`, consistent with the catch-all error handling. Assisted-by: Claude Code Signed-off-by: Miroslav Vadkerti --- testcloud/distro_utils/fedora.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testcloud/distro_utils/fedora.py b/testcloud/distro_utils/fedora.py index 38167a6..7c3d6c9 100644 --- a/testcloud/distro_utils/fedora.py +++ b/testcloud/distro_utils/fedora.py @@ -118,12 +118,11 @@ def get_fedora_image_url(version: str, arch: str) -> str: # get Fedora Cloud url try: aliases = _get_fedora_aliases() + version = _resolve_fedora_version(version, aliases) except Exception: log.error("Couldn't fetch Fedora releases...") raise exceptions.TestcloudImageError - version = _resolve_fedora_version(version, aliases) - if version == "qa-matrix": if arch != "x86_64": log.error("non-x86_64 architecture is not supported with Fedora qa-matrix.")