Source code for rmote.tools.apt_repository

from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path

from rmote.protocol import Tool

_SOURCES_DIR = Path("/etc/apt/sources.list.d")
_KEYRINGS_DIR = Path("/etc/apt/keyrings")


@dataclass
class Result:
    name: str
    changed: bool


[docs] class AptRepository(Tool): """Manage APT repository sources and GPG keys on Debian/Ubuntu. Repository sources are written as DEB822 ``.sources`` files under ``/etc/apt/sources.list.d/``. GPG keyrings are stored in ``/etc/apt/keyrings/``. All operations are idempotent and require root. """
[docs] @staticmethod def key(name: str, data: bytes) -> Result: """Install a GPG signing key to ``/etc/apt/keyrings/{name}``. The operation is idempotent — if the file already contains identical bytes it is left untouched. Args: name: Filename to write the key under (e.g. ``"docker.gpg"``). data: Raw key bytes (binary DER or ASCII-armoured GPG key). Returns: :class:`Result` indicating whether the key was written. """ _KEYRINGS_DIR.mkdir(parents=True, exist_ok=True) path = _KEYRINGS_DIR / name if path.exists() and path.read_bytes() == data: return Result(name=name, changed=False) path.write_bytes(data) return Result(name=name, changed=True)
[docs] @staticmethod def present( name: str, *, types: Iterable[str] = ("deb",), uris: Iterable[str], suites: Iterable[str], components: Iterable[str], signed_by: str = "", ) -> Result: """Ensure a DEB822 ``.sources`` file is configured for *name*. Writes ``/etc/apt/sources.list.d/{name}.sources``. The operation is idempotent — if the file already contains the expected content it is left untouched. Args: name: Repository label used as the filename stem. types: Package types — typically ``("deb",)`` for binary packages or ``("deb-src",)`` for source packages. uris: Repository base URIs (e.g. ``("https://download.docker.com/linux/ubuntu",)``). suites: Distribution suites (e.g. ``("noble",)``). components: Repository components (e.g. ``("stable",)``). signed_by: Absolute path to the GPG keyring that signs this repository. Returns: :class:`Result` indicating whether the file was created or updated. """ lines = [ f"Types: {' '.join(types)}", f"URIs: {' '.join(uris)}", f"Suites: {' '.join(suites)}", f"Components: {' '.join(components)}", ] if signed_by: lines.append(f"Signed-By: {signed_by}") content = "\n".join(lines) + "\n" _SOURCES_DIR.mkdir(parents=True, exist_ok=True) path = _SOURCES_DIR / f"{name}.sources" if path.exists() and path.read_text() == content: return Result(name=name, changed=False) path.write_text(content) return Result(name=name, changed=True)
[docs] @staticmethod def absent(name: str) -> Result: """Remove the DEB822 ``.sources`` file for *name*, if it exists. Args: name: Repository label (the ``{name}.sources`` filename stem). Returns: :class:`Result` indicating whether the file was removed. """ p = _SOURCES_DIR / f"{name}.sources" if p.exists(): p.unlink() return Result(name=name, changed=True) return Result(name=name, changed=False)