feat: add ansible source discovery (local + git)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INVENTORY_NAMES = {"hosts", "inventory", "inventory.yml", "inventory.ini"}
|
||||
|
||||
|
||||
def get_sources(ansible_cfg: dict) -> list[dict]:
|
||||
"""Return resolved source list from ansible config section.
|
||||
|
||||
Each source dict has: name, type, path (resolved local path),
|
||||
url (git only), branch (git only), pull_interval_seconds (git only).
|
||||
|
||||
Config structure in config.yaml::
|
||||
|
||||
ansible:
|
||||
cache_dir: /var/cache/fablednetmon/ansible
|
||||
sources:
|
||||
- name: my-playbooks
|
||||
type: local
|
||||
path: /opt/playbooks
|
||||
- name: infra-repo
|
||||
type: git
|
||||
url: https://github.com/user/infra.git
|
||||
branch: main
|
||||
pull_interval_seconds: 3600
|
||||
"""
|
||||
sources = ansible_cfg.get("sources", [])
|
||||
cache_dir = ansible_cfg.get("cache_dir", "/var/cache/fablednetmon/ansible")
|
||||
result = []
|
||||
for src in sources:
|
||||
src_type = src.get("type", "local")
|
||||
if src_type == "git":
|
||||
path = str(Path(cache_dir) / src["name"])
|
||||
else:
|
||||
path = src.get("path", "")
|
||||
result.append({
|
||||
"name": src["name"],
|
||||
"type": src_type,
|
||||
"path": path,
|
||||
"url": src.get("url"),
|
||||
"branch": src.get("branch", "main"),
|
||||
"pull_interval_seconds": int(src.get("pull_interval_seconds", 3600)),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def discover_playbooks(source_path: str) -> list[str]:
|
||||
"""Recursively find .yml and .yaml files in source_path. Returns relative paths."""
|
||||
root = Path(source_path)
|
||||
if not root.exists():
|
||||
return []
|
||||
playbooks = set()
|
||||
for ext in ("*.yml", "*.yaml"):
|
||||
for p in root.rglob(ext):
|
||||
playbooks.add(str(p.relative_to(root)))
|
||||
return sorted(playbooks)
|
||||
|
||||
|
||||
def discover_inventories(source_path: str) -> list[str]:
|
||||
"""Non-recursive: return inventory filenames present in root of source_path."""
|
||||
root = Path(source_path)
|
||||
if not root.exists():
|
||||
return []
|
||||
return sorted(name for name in INVENTORY_NAMES if (root / name).exists())
|
||||
|
||||
|
||||
def read_playbook(source_path: str, relative_path: str) -> str | None:
|
||||
"""Return contents of a playbook file, or None if not found / path escape."""
|
||||
root = Path(source_path).resolve()
|
||||
target = (root / relative_path).resolve()
|
||||
# Guard against path traversal
|
||||
try:
|
||||
target.relative_to(root)
|
||||
except ValueError:
|
||||
return None
|
||||
if not target.exists() or not target.is_file():
|
||||
return None
|
||||
return target.read_text(errors="replace")
|
||||
|
||||
|
||||
async def git_pull(source: dict) -> None:
|
||||
"""Clone the git repo if absent; pull if already present."""
|
||||
path = Path(source["path"])
|
||||
if not (path / ".git").exists():
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "clone",
|
||||
"--branch", source["branch"],
|
||||
"--single-branch",
|
||||
source["url"], str(path),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.error("git clone failed for %r: %s", source["name"], stderr.decode(errors="replace"))
|
||||
else:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "-C", str(path), "pull",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.error("git pull failed for %r: %s", source["name"], stderr.decode(errors="replace"))
|
||||
Reference in New Issue
Block a user