utils.packages package

Submodules

utils.packages.calls module

The vocabulary a package plan is written in.

A ModuleCall is one Ansible module invocation, a RetryPolicy says how often it may be repeated. Both are plain data, so a plan can be built and asserted without an Ansible connection.

utils.packages.calls.AUR_FETCH = RetryPolicy(attempts=6, seconds=60)

The AUR is one origin host with no mirror pool behind it, so a flap there lasts minutes rather than the seconds a pool member needs to rotate out; EXTERNAL_FETCH’s ~40s window has been observed losing to a single flap.

utils.packages.calls.EXTERNAL_FETCH = RetryPolicy(attempts=3, seconds=10)

Applies to every call that fetches over the network, the distribution’s own repositories included: their metadata is served by mirror pools that degrade independently of the package manager’s internal retries.

utils.packages.calls.GENERIC_PACKAGE = 'ansible.builtin.package'

ansible.builtin.package ships as a documentation-only stub; its logic lives in Ansible’s action plugin. The action plugin here executes modules directly, so it must swap this marker for the host’s ansible_facts.pkg_mgr.

class utils.packages.calls.ModuleCall(module: str, args: dict[str, ~typing.Any]=<factory>, become_user: str | None = None, retry: RetryPolicy | None = None)

Bases: object

One Ansible module invocation the action plugin should execute.

Parameters:
  • module – module name, or GENERIC_PACKAGE for the host’s manager.

  • args – module arguments; a None value is dropped before execution.

  • become_user – user to escalate to, None for the connection default.

  • retry – None when the call is run once, else its retry policy.

args: dict[str, Any]
become_user: str | None = None
module: str
retry: RetryPolicy | None = None
class utils.packages.calls.RetryPolicy(attempts: int, seconds: int)

Bases: object

How often a module call may be repeated, and how long between tries.

Parameters:
  • attempts – total tries, including the first.

  • seconds – pause between tries.

attempts: int
seconds: int
utils.packages.calls.package_call(names: list[str], state: str) → ModuleCall

Install or remove names through the host’s own package manager.

utils.packages.facts module

Read the gathered facts a package plan needs off either fact shape.

utils.packages.facts.distribution_and_family(facts: dict[str, Any]) → tuple[str, str]

Return (distribution, os_family), or ("", "") if either is absent.

utils.packages.facts.read_fact(facts: dict[str, Any], name: str) → str

Read a fact from either the task_vars shape or the setup-module one.

Parameters:
  • facts – an ansible_facts mapping. task_vars carries bare keys, the setup module returns them ansible_-prefixed.

  • name – the unprefixed fact name.

utils.packages.outcome module

Turn the module results of one plan into a single task result.

Pure data in, pure data out, so the reporting is testable without an Ansible connection.

utils.packages.outcome.aggregate(results: list[dict[str, Any]], skipped: list[str], distribution: str) → dict[str, Any]

Fold every module result of one package_install task into one result.

Parameters:
  • results – the module results, in execution order.

  • skipped – package ids that declared nothing for this distribution.

  • distribution – the target distribution, named in the skip reason.

utils.packages.outcome.failure_message(result: dict[str, Any]) → str

Describe one failed module call.

Parameters:

result – the module’s own return dict, tagged with the module that produced it. A module that dies on an unhandled exception never carries msg – the AnsiballZ wrapper returns only failed and exception, whose event holds the sole account of what went wrong.

utils.packages.plan module

Install plans for the package registry.

Turns a resolved PackageSpec into the ordered module calls that acquire it. Keeping this pure means every acquisition path - distribution repository, AUR, COPR, PPA, source build - is unit-testable without an Ansible connection; the action plugin only executes what it is handed.

utils.packages.plan.build_plan(spec: PackageSpec, state: str) → list[ModuleCall]

Return the ordered module calls that bring spec into state.

utils.packages.registry module

Package registry SPOT.

Every installable package is declared exactly once, in one of two places:

  • the repository-root meta/packages.yml holds the shared collection - packages more than one role or an inventory bundle installs;

  • a role’s roles/<role>/meta/packages.yml holds what only that role installs.

This module finds those declarations, indexes them by id and resolves one for a target distribution. The declaration shape itself lives in utils.packages.schema.

class utils.packages.registry.Declaration(package_id: str, role: str | None, path: Path, mapping: dict[str, Any])

Bases: object

One package id, declared either repository-wide or by one role.

mapping: dict[str, Any]
property owner: str

Human-readable owner for lint and runtime messages.

package_id: str
path: Path
role: str | None
property shared: bool

Whether any role may install this id.

utils.packages.registry.build_registry(project_root: Path) → dict[str, Declaration]

Index every declaration by package id.

Raises on a duplicate id: two files declaring the same package is the missing-SPOT condition the uniqueness lint exists to prevent, so the runtime resolver must never silently pick one.

utils.packages.registry.iter_inventory_package_lists(project_root: Path) → Iterator[tuple[Path, list[str]]]

Yield (path, ids) for every inventory that defines PACKAGES.

utils.packages.registry.iter_package_files(project_root: Path) → Iterator[tuple[str | None, Path]]

Yield (role_or_None, path) for every package declaration file.

The shared root file comes first and carries None as its role.

utils.packages.registry.load_declarations(project_root: Path) → list[Declaration]

Read every meta/packages.yml without deduplicating.

Duplicates are returned as-is so the uniqueness lint can report all offenders instead of failing on the first one.

utils.packages.registry.project_root_from_env() → Path

Repository root, overridable for tests via INFINITO_PROJECT_ROOT.

utils.packages.registry.resolve(declaration: Declaration, distro: str, family: str | None = None) → PackageSpec | None

Resolve distro through its distro override, else its os_family.

family is the os_family ansible reports for the host. Callers that have facts (the action plugin) pass it, so distributions outside the CI matrix - Manjaro, Mint, Rocky - still resolve through their family. Callers without facts (lints, the availability test) omit it and the family is derived from DISTRO_FAMILY, which only covers the default matrix.

Returns None when neither key is declared, which is what the coverage lint reports as a gap.

utils.packages.schema module

Declaration shape for meta/packages.yml.

Owns what a package entry may say and how one distro entry becomes a concrete PackageSpec. Finding and indexing the declarations is utils.packages.registry; turning a spec into module calls is utils.packages.plan.

The same shape serves the repository-root file, which holds the shared collection any role and any inventory bundle may install from, and a role’s own file, which holds what only that role installs.

The file is a mapping of logical package id to a distro mapping. Keys are ansible_facts['os_family'] values; a lowercase distribution name (ubuntu, centos, fedora, arch, debian) overrides its family when that one distribution differs:

nfs-ganesha:
  Debian: [nfs-ganesha, nfs-ganesha-vfs]
  RedHat: [nfs-ganesha, nfs-ganesha-vfs]
  centos:
    names: [nfs-ganesha, nfs-ganesha-vfs]
    repo:
      bootstrap_package: centos-release-nfs-ganesha11
  Archlinux:
    source: aur
    names: [nfs-ganesha]

repo carries the repository the names come from, defined inline on the entry that needs it: a full definition (name/description/ baseurl/gpgkey) rendered as a yum repo, bootstrap_package for a repo shipped as a package such as epel-release, enable_existing to switch on a repo the distro already ships, pacman_section to uncomment a section of pacman.conf such as multilib, copr/ppa for a community repository, or managed_externally when the role sets it up itself.

A bare list is shorthand for {names: [...], source: repo}. An empty list states that the package is deliberately not installed on that family, which counts as covered; a missing key is a coverage gap.

virtual: true records that the package manager satisfies the name through an RPM Provides rather than a package of that name, so an index lookup by name legitimately finds nothing while the install works.

utils.packages.schema.DISTRO_FAMILY: dict[str, str] = {'arch': 'Archlinux', 'centos': 'RedHat', 'debian': 'Debian', 'fedora': 'RedHat', 'ubuntu': 'Debian'}

The default distro matrix mapped onto the os_family ansible reports for it, read from the meta/distros.yml SPOT. Coverage is required for every key.

utils.packages.schema.INVENTORY_PACKAGES_VAR = 'PACKAGES'

Inventory variable listing extra package ids a bundle installs.

class utils.packages.schema.PackageSpec(names: tuple[str, ...], source: str = 'repo', repo: dict[str, Any] | None = None, virtual: bool = False, build: dict[str, Any] | None = None)

Bases: object

Resolved install instruction for one package on one distribution.

build: dict[str, Any] | None = None
names: tuple[str, ...]
repo: dict[str, Any] | None = None
source: str = 'repo'
virtual: bool = False
exception utils.packages.schema.PackagesShapeError

Bases: ValueError

Raised when a meta/packages.yml violates the declared shape.

utils.packages.schema.REPO_KEYS: frozenset[str] = frozenset({'baseurl', 'bootstrap_package', 'copr', 'enable_existing', 'managed_externally', 'pacman_section', 'ppa'})

A repository definition must declare at least one of these; the rest (name, description, gpgkey, gpgcheck, state) qualify it.

utils.packages.schema.SOURCES: tuple[str, ...] = ('repo', 'aur', 'copr', 'ppa', 'build')

How the names are acquired. repo is the distribution’s own package manager; aur, copr and ppa are the community managers Arch, Fedora and Ubuntu ship alongside it; build compiles from source where no manager carries the package at all.

utils.packages.schema.normalize_entry(package_id: str, key: str, value: Any, path: Path) → PackageSpec

Turn a bare list or a spec mapping into a PackageSpec.

utils.packages.sources module

One builder per acquisition path.

Each function returns the ordered calls that bring a package in from one source. utils.packages.plan picks between them; none of them knows about the others.

utils.packages.sources.AUR_MAKEPKG_ARGS = '--skipinteg'

The AUR PKGBUILDs this project consumes carry a checksum on a git source, which makepkg can only reject; integrity rests on the pinned upstream tag fetched over HTTPS.

utils.packages.sources.aur_calls(names: list[str]) → list[ModuleCall]

Prepare the unprivileged build environment, then build through it.

makepkg refuses to run as root, so the toolchain, a build user and a passwordless pacman rule must exist before any AUR package is built.

utils.packages.sources.build_calls(build: dict[str, Any]) → list[ModuleCall]

Install the declared dependencies, then run the build command.

utils.packages.sources.pacman_section_calls(section: str, names: list[str]) → list[ModuleCall]

Uncomment a pacman.conf section, then install through it.

Arch ships multilib commented out, so the repository holding the 32-bit and Steam packages does not exist until the two lines are enabled. The regex stops matching once uncommented, which is what makes it idempotent.

utils.packages.sources.repository_calls(repo: dict[str, Any], names: list[str]) → list[ModuleCall]

Enable the declared repository, then install through it.

Module contents