Files
computer-playbook/utils/paths.py
T
kevinveenbirkenbachandClaude Opus 5 104ef5e9d0 fix(compose): keep the runtime's resolver so a compose alias still resolves
Naming a dns server replaces the runtime's embedded one rather than adding
to it, and that embedded server is the only thing answering compose
aliases. The dev container could therefore not look up `tools`, so every
check reaching a lane service by its service name reported that nothing
answers and skipped itself: the System One suite skipped with 325 usable
pairs behind an address it could not resolve, while its service was up and
healthy.

The embedded resolver goes after the platform one, so platform names
resolve where they always did and only what that server does not know falls
through.

Its address is the one group_vars/all/08_networks.yml already holds, read
through the env pipeline rather than written a second time in compose.yml.
`read_group_path` only ever read the paths file, so the reader it is built
on now takes the file to read and it keeps its own signature.

The two docker handlers share a prefix once a second one exists, which is a
folder; they move into one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 11:45:55 +02:00

117 lines
3.7 KiB
Python

"""Filesystem SPOT for the Python side, read from group_vars/all/05_paths.yml.
``group_vars/all/05_paths.yml`` is the single source of truth for host paths.
The env layer (utils/env/handlers, exported by scripts/meta/env/load.sh) derives
the ``INFINITO_*`` env keys from it; a set env wins so callers can override.
When a tool runs outside that layer, e.g. a bare ``subprocess`` that does not
inherit the env build, the value is read straight from the group_vars SPOT, so
there is still no hardcoded literal default.
The SPOT is parsed with a plain-string line parser instead of PyYAML: the env
build runs during bootstrap (make install-system-python) before any Python
dependency exists, so this module must stay stdlib-only. Only plain
``KEY: "value"`` entries can be read this way; a Jinja expression or a missing
key is a hard error.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
from utils import PROJECT_ROOT
from utils.cache.files import read_text
_GROUP_PATHS_FILE = str(PROJECT_ROOT / "group_vars" / "all" / "05_paths.yml")
_ENTRY_RE = re.compile(
r'^(?P<key>[A-Za-z_][A-Za-z0-9_]*):\s*"?(?P<value>[^"#]*)"?\s*(?:#.*)?$'
)
_ITEM_RE = re.compile(r'^\s+-\s*"?(?P<value>[^"#]*)"?\s*(?:#.*)?$')
def read_group_path(key: str) -> str:
"""Plain-string path value from the group_vars paths SPOT.
Args:
key: top-level variable name in group_vars/all/05_paths.yml.
Returns:
The literal string value.
Raises:
KeyError: the key is not defined in the SPOT.
ValueError: the value is not a plain string (e.g. a Jinja template).
"""
return read_group_value(_GROUP_PATHS_FILE, key)
def read_group_value(path: str, key: str) -> str:
"""Plain-string value of ``key`` in a group_vars file.
Args:
path: the group_vars file holding the value.
key: top-level variable name in it.
Returns:
The literal string value.
Raises:
KeyError: the key is not defined in that file.
ValueError: the value is not a plain string (e.g. a Jinja template).
"""
for line in read_text(path).splitlines():
match = _ENTRY_RE.match(line.strip())
if not match or match.group("key") != key:
continue
value = match.group("value").strip()
if not value or "{{" in value:
raise ValueError(
f"{key} in {path} must be a plain string, got: {line.strip()!r}"
)
return value
raise KeyError(f"{key} not defined in {path}")
def read_group_paths(key: str) -> list[str]:
"""Plain-string list value from the group_vars paths SPOT.
Args:
key: top-level variable name whose value is a YAML block sequence.
Returns:
The literal string entries, in file order.
Raises:
KeyError: the key is not defined in the SPOT or carries no entry.
"""
entries: list[str] = []
collecting = False
for line in read_text(_GROUP_PATHS_FILE).splitlines():
if line.startswith(f"{key}:"):
collecting = True
continue
if not collecting:
continue
item = _ITEM_RE.match(line)
if item is None:
break
entries.append(item.group("value").strip())
if not entries:
raise KeyError(f"{key} not defined as a list in {_GROUP_PATHS_FILE}")
return entries
def _dir_var_lib() -> str:
env = os.environ.get("INFINITO_DIR_VAR_LIB")
if env:
return env
return read_group_path("DIR_VAR_LIB")
DIR_VAR_LIB = Path(_dir_var_lib())
DIR_BACKUPS = DIR_VAR_LIB / "backup"
DIR_SECRETS = DIR_VAR_LIB / "secrets"
FILE_TOKENS = DIR_SECRETS / "tokens.yml"
FILE_DATABASE_SECRETS = DIR_SECRETS / "databases.csv"