Files
computer-playbook/utils/tests/swarm/extend_inventory.py

163 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Extend the provisioned test inventory with host-topology placements
plus the dep-walked ``placement: manager`` set, so the static
validator (running before ansible) finds every group the swarm deploy
will use.
The dep walk delegates to ``derive_includes`` so the group set matches
the provisioned include/credential closure.
Node base names are the SPOT in default.env (shared with scripts/tests/deploy/swarm/utils/topology/base.sh);
only the SWARM_NAME prefix is applied here so host names match the
containers. The env get() keeps SWARM_NAME import-safe; main() enforces it
at run time.
The backup host is NOT appended to the cluster inventory: it lives in a
sibling ``backup.yml`` next to ``INV_PATH`` (same host_vars dir), deployed
as a second play after the cluster one. Without a svc-swarm-node group that
play resolves DEPLOYMENT_MODE=compose and IS_STACK_HOST=true on the backup
host, so the backup roles run without cluster-membership special cases.
Inputs (env): ``APP_ID``, ``INV_PATH`` (default ``/tmp/inv/devices.yml``).
Optional ``INFINITO_APP_VARIANTS`` (consumed by ``derive_includes``).
"""
from __future__ import annotations
import os
from pathlib import Path
from utils import PROJECT_ROOT
from utils.cache.yaml import dump_yaml, load_yaml_any
from utils.env.parser import parse_static_env
from utils.roles.meta_lookup import get_role_placement
from utils.tests.swarm.backup_repos import repo_placements
from utils.tests.swarm.derive_includes import applications_for_round, derive_includes
_DOCKER_VARS: dict[str, str] = {
"ansible_connection": "docker",
"ansible_python_interpreter": "/usr/bin/python3",
"ansible_user": "root",
}
_NAMES = parse_static_env(PROJECT_ROOT / "default.env")
_PREFIX = f"{os.environ['SWARM_NAME']}-" if os.environ.get("SWARM_NAME") else ""
_MANAGER = f"{_PREFIX}{_NAMES['INFINITO_SWARM_MGR_NAME']}"
def mesh_enabled() -> bool:
return (os.environ.get("INFINITO_SWARM_VPN") or "").strip().lower() == "true"
_WORKERS = (
f"{_PREFIX}{_NAMES['INFINITO_SWARM_WRK1_NAME']}",
f"{_PREFIX}{_NAMES['INFINITO_SWARM_WRK2_NAME']}",
)
_NFS_SERVER = f"{_PREFIX}{_NAMES['INFINITO_SWARM_NFS_NAME']}"
_BACKUP = f"{_PREFIX}{_NAMES['INFINITO_SWARM_BACKUP_NAME']}"
def _host_topology(app_id: str) -> list[tuple[str, str]]:
app_hosts: list[tuple[str, str]] = [(app_id, _MANAGER)]
# Exception: svc-swarm-manager is the IS_STACK_HOST marker group; leaking
# workers into it flips IS_STACK_HOST true on workers and skips the CA
# fetch. Keep it manager-only.
if app_id != "svc-swarm-manager" and get_role_placement(app_id) != "manager":
app_hosts.extend((app_id, w) for w in _WORKERS)
mesh_hosts: list[tuple[str, str]] = []
if mesh_enabled():
mesh_hosts = [
("svc-net-wireguard", _MANAGER),
*[("svc-net-wireguard", w) for w in _WORKERS],
("svc-net-wireguard", _NFS_SERVER),
]
return [
("svc-swarm-node", _MANAGER),
*[("svc-swarm-node", w) for w in _WORKERS],
("svc-swarm-manager", _MANAGER),
("svc-storage-nfs-client", _MANAGER),
*[("svc-storage-nfs-client", w) for w in _WORKERS],
("svc-storage-nfs-server", _NFS_SERVER),
*mesh_hosts,
*app_hosts,
]
def _placement_dep_groups(app_id: str) -> list[tuple[str, str]]:
return [
(role_name, _MANAGER)
for role_name in derive_includes(app_id)
if get_role_placement(role_name) == "manager"
]
def _sandbox_workers(app_id: str) -> tuple[str, ...]:
"""Return the workers that may carry the kata-capable label.
Args:
app_id: the round's primary application.
Returns:
Every worker when the app itself is sandboxed, otherwise one.
"""
services = (applications_for_round().get(app_id) or {}).get("services") or {}
return (
_WORKERS
if ((services.get("kata") or {}).get("enabled")) is True
else _WORKERS[-1:]
)
def main() -> int:
if not os.environ.get("SWARM_NAME"):
raise SystemExit("extend_inventory: SWARM_NAME is required (cluster id)")
app_id = os.environ["APP_ID"]
inv_path = Path(os.environ.get("INV_PATH", "/tmp/inv/devices.yml")) # noqa: S108 - ephemeral swarm-test inventory path, overridable via INV_PATH
closure = derive_includes(app_id)
group_hosts = _host_topology(app_id) + _placement_dep_groups(app_id)
if "svc-virt-kata" in closure:
group_hosts.extend(
("svc-virt-kata", host) for host in (_MANAGER, *_sandbox_workers(app_id))
)
group_hosts.extend(
repo_placements(
app_closure=closure,
nfs_closure=derive_includes("svc-storage-nfs-server"),
manager=_MANAGER,
nfs_server=_NFS_SERVER,
)
)
inv = load_yaml_any(str(inv_path), default_if_missing={})
inv.setdefault("all", {}).setdefault("children", {})
children = inv["all"]["children"]
for group, host in group_hosts:
children.setdefault(group, {}).setdefault("hosts", {})
children[group]["hosts"][host] = dict(_DOCKER_VARS)
dump_yaml(str(inv_path), inv)
print(inv_path.read_text()) # nocheck: cache-read — re-reads the file just written
backup_inv = {
"all": {
"children": {
group: {"hosts": {_BACKUP: dict(_DOCKER_VARS)}}
for group in (
"svc-bkp-remote-2-local",
"svc-bkp-local-2-device",
*(("svc-net-wireguard",) if mesh_enabled() else ()),
)
}
}
}
backup_path = inv_path.parent / "backup.yml"
dump_yaml(str(backup_path), backup_inv)
print(backup_path.read_text()) # nocheck: cache-read
return 0
if __name__ == "__main__":
raise SystemExit(main())