Files
computer-playbook/cli/create/inventory/credentials_generator.py
T

166 lines
4.9 KiB
Python

from __future__ import annotations
import concurrent.futures
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
import sys
from .role_resolver import resolve_role_path
def _fatal(msg: str) -> None:
raise SystemExit(f"[FATAL] {msg}")
def _ensure_ruamel_map(node: CommentedMap, key: str) -> CommentedMap:
if key not in node or not isinstance(node.get(key), CommentedMap):
node[key] = CommentedMap()
return node[key]
def _generate_credentials_snippet_for_app(
app_id: str,
roles_dir: Path,
host_vars_file: Path,
vault_password_file: Path,
project_root: Path,
env: Optional[Dict[str, str]],
) -> Optional[CommentedMap]:
role_path = resolve_role_path(app_id, roles_dir, project_root, env=env)
if role_path is None:
return None
cmd = [
# Use current interpreter so utils + deps match the runtime
sys.executable,
"-m",
"cli.create.credentials",
"--role-path",
str(role_path),
"--inventory-file",
str(host_vars_file),
"--vault-password-file",
str(vault_password_file),
"--snippet",
"--allow-empty-plain",
]
result = subprocess.run(cmd, text=True, capture_output=True, env=env)
if result.returncode != 0:
_fatal(
f"Command failed ({result.returncode}): {' '.join(cmd)}\n"
f"STDOUT:\n{result.stdout or ''}\nSTDERR:\n{result.stderr or ''}"
)
snippet_text = (result.stdout or "").strip()
if not snippet_text:
return None
yaml_rt = YAML(typ="rt")
try:
data = yaml_rt.load(snippet_text)
except Exception as exc:
raise SystemExit(f"Failed to parse credentials snippet for {app_id}: {exc}")
if data is None:
return None
if not isinstance(data, CommentedMap):
cm = CommentedMap()
for k, v in dict(data).items():
cm[k] = v
return cm
return data
def generate_credentials_for_roles(
application_ids: List[str],
roles_dir: Path,
host_vars_file: Path,
vault_password_file: Path,
project_root: Path,
env: Optional[Dict[str, str]],
workers: int = 4,
) -> None:
if not application_ids:
return
max_workers = max(1, workers)
snippets: List[CommentedMap] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_app: Dict[concurrent.futures.Future, str] = {}
for app_id in application_ids:
future = executor.submit(
_generate_credentials_snippet_for_app,
app_id,
roles_dir,
host_vars_file,
vault_password_file,
project_root,
env,
)
future_to_app[future] = app_id
for future in concurrent.futures.as_completed(future_to_app):
app_id = future_to_app[future]
try:
snippet = future.result()
except Exception as exc:
_fatal(f"Worker for {app_id} failed with exception: {exc}")
if snippet is not None:
snippets.append(snippet)
if not snippets:
return
yaml_rt = YAML(typ="rt")
yaml_rt.preserve_quotes = True
if host_vars_file.exists():
with host_vars_file.open("r", encoding="utf-8") as f:
doc = yaml_rt.load(f)
if doc is None:
doc = CommentedMap()
else:
doc = CommentedMap()
if not isinstance(doc, CommentedMap):
tmp = CommentedMap()
for k, v in dict(doc).items():
tmp[k] = v
doc = tmp
for snippet in snippets:
apps_snip = snippet.get("applications", {}) or {}
if isinstance(apps_snip, dict):
apps_doc = _ensure_ruamel_map(doc, "applications")
for app_id, app_block_snip in apps_snip.items():
if not isinstance(app_block_snip, dict):
continue
app_doc = _ensure_ruamel_map(apps_doc, app_id)
creds_doc = _ensure_ruamel_map(app_doc, "credentials")
creds_snip = app_block_snip.get("credentials", {}) or {}
if not isinstance(creds_snip, dict):
continue
for key, val in creds_snip.items():
if key not in creds_doc or (
creds_doc.get(key) == "" and val not in ("", None)
):
creds_doc[key] = val
if (
"ansible_become_password" in snippet
and "ansible_become_password" not in doc
):
doc["ansible_become_password"] = snippet["ansible_become_password"]
host_vars_file.parent.mkdir(parents=True, exist_ok=True)
with host_vars_file.open("w", encoding="utf-8") as f:
yaml_rt.dump(doc, f)