Skip to content

podman_info

podman_info

Podman environment detection.

Parses podman info -f json output and containers.conf to detect podman capabilities, version, and hooks directory configuration.

This module is stateless — callers cache the result.

PodmanInfo(version, rootless_network_cmd, pasta_executable, slirp4netns_executable) dataclass

Parsed podman environment information.

Constructed from podman info -f json output. Stateless — the caller manages caching.

hooks_dir_persists property

Return True if --hooks-dir survives container restart.

Currently always False — podman drops per-container hooks-dir on stop/start even on 5.8.0 (issues #121, #122). The version gate will be lowered when podman fixes this upstream.

network_mode property

Determine the rootless network mode.

Uses rootlessNetworkCmd when available (podman 5+). When absent (podman 4.x), defaults to slirp4netns if its executable is available — podman 4.x defaults to slirp4netns.

parse_podman_info(json_str)

Parse podman info -f json output into a :class:PodmanInfo.

Returns a zero-version fallback on invalid input.

Source code in src/terok_shield/common/podman_info.py
def parse_podman_info(json_str: str) -> PodmanInfo:
    """Parse ``podman info -f json`` output into a :class:`PodmanInfo`.

    Returns a zero-version fallback on invalid input.
    """
    try:
        info = json.loads(json_str)
    except (json.JSONDecodeError, TypeError):
        return PodmanInfo(
            version=(0,),
            rootless_network_cmd="",
            pasta_executable="",
            slirp4netns_executable="",
        )

    host = info.get("host", {}) if isinstance(info, dict) else {}
    version_section = info.get("version", {}) if isinstance(info, dict) else {}

    return PodmanInfo(
        version=_parse_version(version_section.get("Version", "0")),
        rootless_network_cmd=host.get("rootlessNetworkCmd", ""),
        pasta_executable=host.get("pasta", {}).get("executable", ""),
        slirp4netns_executable=host.get("slirp4netns", {}).get("executable", ""),
    )

find_hooks_dirs()

Find hooks directories podman would check.

Reads containers.conf (user config overrides system config). Falls back to well-known system defaults if nothing is configured.

Returns directories in precedence order (last wins for podman).

Source code in src/terok_shield/common/podman_info.py
def find_hooks_dirs() -> list[Path]:
    """Find hooks directories podman would check.

    Reads ``containers.conf`` (user config overrides system config).
    Falls back to well-known system defaults if nothing is configured.

    Returns directories in precedence order (last wins for podman).
    """
    # User config takes precedence over system config
    user_dirs = _parse_hooks_dir_from_conf(_user_containers_conf())
    if user_dirs:
        return [Path(d).expanduser() for d in user_dirs]

    # System configs (checked in order, last found wins)
    for conf_path in reversed(_SYSTEM_CONF_PATHS):
        dirs = _parse_hooks_dir_from_conf(conf_path)
        if dirs:
            return [Path(d).expanduser() for d in dirs]

    # No config → well-known system defaults (only existing ones)
    return [d for d in _SYSTEM_HOOKS_DIRS if d.is_dir()]

has_global_hooks(hooks_dirs=None)

Check if terok-shield hooks are installed in any global hooks dir.

Parameters:

Name Type Description Default
hooks_dirs list[Path] | None

Directories to check (default: auto-detect via :func:find_hooks_dirs).

None
Source code in src/terok_shield/common/podman_info.py
def has_global_hooks(hooks_dirs: list[Path] | None = None) -> bool:
    """Check if terok-shield hooks are installed in any global hooks dir.

    Args:
        hooks_dirs: Directories to check (default: auto-detect via
            :func:`find_hooks_dirs`).
    """
    if hooks_dirs is None:
        hooks_dirs = find_hooks_dirs()
    return any((d / HOOK_JSON_FILENAME).is_file() for d in hooks_dirs)

parse_resolv_conf(text)

Extract the first nameserver address from resolv.conf content.

Returns an empty string if no valid nameserver line is found.

Source code in src/terok_shield/common/podman_info.py
def parse_resolv_conf(text: str) -> str:
    """Extract the first ``nameserver`` address from resolv.conf content.

    Returns an empty string if no valid nameserver line is found.
    """
    for line in text.splitlines():
        parts = line.strip().split()
        if len(parts) >= 2 and parts[0] == "nameserver":
            return parts[1]
    return ""

parse_proc_net_route(text)

Extract the default gateway IP from /proc/{pid}/net/route content.

The gateway field is a 32-bit hex integer in host byte order. Returns an empty string if no default route is found.

Source code in src/terok_shield/common/podman_info.py
def parse_proc_net_route(text: str) -> str:
    """Extract the default gateway IP from ``/proc/{pid}/net/route`` content.

    The gateway field is a 32-bit hex integer in host byte order.
    Returns an empty string if no default route is found.
    """
    import socket
    import struct

    for line in text.splitlines()[1:]:  # skip header
        fields = line.split()
        if len(fields) >= 3 and fields[1] == "00000000":  # default route
            try:
                gw_int = int(fields[2], 16)
                return socket.inet_ntoa(struct.pack("=I", gw_int))
            except (ValueError, struct.error):
                continue
    return ""

system_hooks_dir()

Return the best system-level hooks directory.

Prefers existing directories; falls back to /etc/containers/oci/hooks.d.

Source code in src/terok_shield/common/podman_info.py
def system_hooks_dir() -> Path:
    """Return the best system-level hooks directory.

    Prefers existing directories; falls back to ``/etc/containers/oci/hooks.d``.
    """
    for d in _SYSTEM_HOOKS_DIRS:
        if d.is_dir():
            return d
    return _SYSTEM_HOOKS_DIRS[-1]

global_hooks_hint()

Short hint telling the user to run terok-shield setup.

Source code in src/terok_shield/common/podman_info.py
def global_hooks_hint() -> str:
    """Short hint telling the user to run ``terok-shield setup``."""
    return (
        "Per-container --hooks-dir does not persist on container restart\n"
        "(ref: https://github.com/containers/podman/issues/17935).\n"
        "\n"
        "Run 'terok-shield setup' to install global hooks."
    )

ensure_containers_conf_hooks_dir(hooks_dir)

Ensure ~/.config/containers/containers.conf includes hooks_dir.

Creates the file if absent. Inserts hooks_dir into the existing [engine] section, or appends a new section if none exists. Warns (does not fail) if hooks_dir is already set differently.

Uses line-based text manipulation to preserve comments and formatting.

Source code in src/terok_shield/common/podman_info.py
def ensure_containers_conf_hooks_dir(hooks_dir: Path) -> None:
    """Ensure ``~/.config/containers/containers.conf`` includes *hooks_dir*.

    Creates the file if absent.  Inserts ``hooks_dir`` into the existing
    ``[engine]`` section, or appends a new section if none exists.
    Warns (does not fail) if ``hooks_dir`` is already set differently.

    Uses line-based text manipulation to preserve comments and formatting.
    """
    conf_path = _user_containers_conf()
    hooks_str = str(hooks_dir)
    hooks_line = f'hooks_dir = ["{hooks_str}"]'

    if not conf_path.is_file():
        conf_path.parent.mkdir(parents=True, exist_ok=True)
        conf_path.write_text(f"[engine]\n{hooks_line}\n")
        return

    existing = _parse_hooks_dir_from_conf(conf_path)
    if not existing:
        _insert_hooks_line(conf_path, hooks_line)
        return

    if hooks_str in existing or str(hooks_dir.expanduser()) in existing:
        return  # already configured
    print(
        f"Warning: {conf_path} already has hooks_dir = {existing}\n"
        f"Add {hooks_str!r} to the list manually if needed."
    )