Files
computer-playbook/utils/packages/plan.py
T
kevinveenbirkenbachandClaude Opus 5.5 18ea75049e perf(packages): install every role's packages per host in one run at deploy start
A constructor task after the update role and the swarm auto-placement installs,
in one package-manager call, every package id of every role the host's groups
may run: meta dependencies, service edges and every role invocation readable
from tasks and handlers, conditions ignored. Ids that must stay with their role
(a conditional install, a repository added first, a need before update) carry
batch: false in meta/packages.yml and keep their task; roles reference shared
root ids with shared: true. 42 per-role install tasks go, and a lint keeps the
split: no batched id in a role task, none in a role run ahead of the batch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 22:54:36 +01:00

100 lines
3.2 KiB
Python

"""Install plans for the package registry.
Turns a resolved :class:`~utils.packages.registry.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.
"""
from __future__ import annotations
from dataclasses import replace
from utils.packages.calls import (
GENERIC_PACKAGE,
STATE_ABSENT,
STATE_PRESENT,
ModuleCall,
package_call,
)
from utils.packages.schema import (
SOURCE_AUR,
SOURCE_BUILD,
SOURCE_COPR,
SOURCE_PPA,
PackageSpec,
)
from utils.packages.sources import aur_calls, build_calls, repository_calls
def build_plan(spec: PackageSpec, state: str) -> list[ModuleCall]:
"""Return the ordered module calls that bring ``spec`` into ``state``."""
names = list(spec.names)
if not names:
return []
if state == STATE_ABSENT:
return [package_call(names, STATE_ABSENT)]
if spec.source == SOURCE_AUR:
return aur_calls(names)
if spec.source == SOURCE_BUILD:
if not spec.build:
raise ValueError("source 'build' requires a 'build' block")
return build_calls(spec.build)
if spec.source in (SOURCE_COPR, SOURCE_PPA) and not spec.repo:
raise ValueError(f"source '{spec.source}' requires a 'repo' block")
if spec.repo:
return repository_calls(spec.repo, names)
return [package_call(names, STATE_PRESENT)]
def _plain(plan: list[ModuleCall]) -> bool:
if not plan or any(
call.module != GENERIC_PACKAGE
or call.become_user is not None
or set(call.args) != {"name", "state"}
for call in plan
):
return False
return len({call.args["state"] for call in plan}) == 1
def merge_plans(plans: list[list[ModuleCall]]) -> list[list[ModuleCall]]:
"""Fold the plans that only call the host's package manager into one plan.
The plain plans are aligned on their last call, so the n-th call from
the end of every plan lands in the same merged call and each plan's own
order survives: a bootstrap package still installs before the names it
unlocks. Every other plan follows unchanged, in request order.
Args:
plans: one plan per package id, in request order.
Returns:
one merged plan per state the plain plans bring their names into,
then every other plan.
"""
groups: dict[str, list[list[ModuleCall]]] = {}
for plan in plans:
if _plain(plan):
groups.setdefault(plan[-1].args["state"], []).append(plan)
return [_merge(group) for group in groups.values()] + [
plan for plan in plans if not _plain(plan)
]
def _merge(plain: list[list[ModuleCall]]) -> list[ModuleCall]:
merged: list[ModuleCall] = []
for offset in range(max(len(plan) for plan in plain), 0, -1):
calls = [plan[-offset] for plan in plain if len(plan) >= offset]
names = list(
dict.fromkeys(name for call in calls for name in call.args["name"])
)
merged.append(replace(calls[0], args={**calls[0].args, "name": names}))
return merged