Source code for repoman.checker

"""Availability checker: tests repos against a target Ubuntu codename.

Runs entirely in a background thread. Never touches GTK widgets.
Uses Launchpad API for PPAs and HTTP HEAD for all other repos.
A single network failure sets a global flag that suppresses further attempts,
so one DNS outage does not cause a storm of error toasts.
"""

from __future__ import annotations

import logging

import requests

_log = logging.getLogger(__name__)

from .models import AvailabilityStatus, Repository

# Populated lazily; set to True on first network error to suppress further attempts
_network_failed = False
_network_error_message: str = ""


[docs] def reset_network_state() -> None: """Reset the network failure flag — useful between wizard runs.""" global _network_failed, _network_error_message _network_failed = False _network_error_message = ""
[docs] def get_network_error() -> str: """Return the last network error message, or empty string if none.""" return _network_error_message
[docs] class Checker: """ Checks repo availability for a given Ubuntu codename. Run in a background thread — must not touch GTK widgets. On the first network error (timeout, DNS failure, HTTP error), the checker marks the global network-failed flag so the UI can surface a single alert and all subsequent checks return UNKNOWN immediately. """
[docs] def check(self, repo: Repository, codename: str) -> AvailabilityStatus: """Check whether ``repo`` has packages published for ``codename``. Suite-agnostic repos always return SUITE_AGNOSTIC without a network call. If the global network-failure flag is set from a previous call, returns UNKNOWN immediately to avoid a cascade of timeouts. :param repo: The repository to check. :type repo: Repository :param codename: Ubuntu codename to check against, e.g. ``"noble"``. :type codename: str :returns: Availability status for this repo/codename pair. :rtype: AvailabilityStatus """ global _network_failed if repo.availability == AvailabilityStatus.SUITE_AGNOSTIC: return AvailabilityStatus.SUITE_AGNOSTIC if _network_failed: return AvailabilityStatus.UNKNOWN if repo.is_ppa: return self._check_launchpad(repo, codename) return self._check_http(repo, codename)
def _check_launchpad(self, repo: Repository, codename: str) -> AvailabilityStatus: """Query Launchpad API for published sources in the given distro series.""" global _network_failed, _network_error_message try: from launchpadlib.launchpad import Launchpad # type: ignore[import] lp = Launchpad.login_anonymously( "repoman", "production", version="devel", ) owner = lp.people[repo.ppa_owner] archive = owner.getPPAByName(name=repo.ppa_name) series = lp.distributions["ubuntu"].getSeries(name_or_version=codename) sources = archive.getPublishedSources( distro_series=series, status="Published", ) return AvailabilityStatus.AVAILABLE if sources.total_size > 0 else AvailabilityStatus.UNAVAILABLE except KeyError: # PPA or series not found return AvailabilityStatus.UNAVAILABLE except Exception as exc: _log.debug("launchpad check failed for %s/%s", repo.ppa_owner, repo.ppa_name, exc_info=True) _network_failed = True _network_error_message = str(exc) return AvailabilityStatus.UNKNOWN def _check_http(self, repo: Repository, codename: str) -> AvailabilityStatus: """HEAD request to {uri}/dists/{codename}/InRelease.""" global _network_failed, _network_error_message try: uri = repo.uris[0].rstrip("/") url = f"{uri}/dists/{codename}/InRelease" response = requests.head(url, timeout=10, allow_redirects=True) if response.status_code == 200: return AvailabilityStatus.AVAILABLE if response.status_code == 404: return AvailabilityStatus.UNAVAILABLE # Other HTTP errors (403, 5xx, etc.) — treat as unknown return AvailabilityStatus.UNKNOWN except requests.exceptions.Timeout: _network_failed = True _network_error_message = f"Connection timed out checking {repo.uris[0]}" return AvailabilityStatus.UNKNOWN except requests.exceptions.ConnectionError: # DNS failure or refused connection — this specific host is unreachable, # not a general network outage. Treat the repo as unavailable. return AvailabilityStatus.UNAVAILABLE except requests.exceptions.RequestException as exc: _network_failed = True _network_error_message = str(exc) return AvailabilityStatus.UNKNOWN