Files

257 lines
8.9 KiB
Python

"""CRUD routes for Ansible inventory: groups and targets."""
from __future__ import annotations
import uuid
import yaml
from quart import Blueprint, current_app, redirect, render_template, request, url_for
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from steward.auth.middleware import require_role
from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget
from steward.models.users import UserRole
inventory_bp = Blueprint("ansible_inventory", __name__, url_prefix="/ansible/inventory")
def _parse_ansible_vars(raw: str) -> dict:
"""Parse a YAML/JSON string into a dict. Raises ValueError on invalid input."""
raw = raw.strip()
if not raw:
return {}
try:
parsed = yaml.safe_load(raw)
except yaml.YAMLError as exc:
raise ValueError(f"Invalid YAML: {exc}") from exc
if parsed is None:
return {}
if not isinstance(parsed, dict):
raise ValueError("ansible_vars must be a YAML mapping (key: value pairs)")
return parsed
# ---------------------------------------------------------------------------
# Groups
# ---------------------------------------------------------------------------
@inventory_bp.get("/groups")
@require_role(UserRole.viewer)
async def groups_list():
async with current_app.db_sessionmaker() as db:
result = await db.execute(
select(AnsibleGroup)
.options(selectinload(AnsibleGroup.targets))
.order_by(AnsibleGroup.name)
)
groups = result.scalars().all()
return await render_template("ansible/inventory/groups.html", groups=groups)
@inventory_bp.post("/groups")
@require_role(UserRole.operator)
async def groups_create():
form = await request.form
name = form.get("name", "").strip()
if not name:
return "name is required", 400
try:
ansible_vars = _parse_ansible_vars(form.get("ansible_vars", ""))
except ValueError as exc:
return str(exc), 400
group = AnsibleGroup(id=str(uuid.uuid4()), name=name, ansible_vars=ansible_vars)
async with current_app.db_sessionmaker() as db:
async with db.begin():
db.add(group)
return redirect(url_for("ansible_inventory.groups_list"))
@inventory_bp.get("/groups/<group_id>")
@require_role(UserRole.viewer)
async def group_detail(group_id: str):
async with current_app.db_sessionmaker() as db:
result = await db.execute(
select(AnsibleGroup)
.where(AnsibleGroup.id == group_id)
.options(selectinload(AnsibleGroup.targets))
)
group = result.scalar_one_or_none()
if group is None:
return "Group not found", 404
all_targets = (
await db.execute(select(AnsibleTarget).order_by(AnsibleTarget.name))
).scalars().all()
member_ids = {t.id for t in group.targets}
ansible_vars_yaml = yaml.dump(group.ansible_vars) if group.ansible_vars else ""
return await render_template(
"ansible/inventory/group_detail.html",
group=group,
all_targets=all_targets,
member_ids=member_ids,
ansible_vars_yaml=ansible_vars_yaml,
)
@inventory_bp.post("/groups/<group_id>")
@require_role(UserRole.operator)
async def group_update(group_id: str):
form = await request.form
try:
ansible_vars = _parse_ansible_vars(form.get("ansible_vars", ""))
except ValueError as exc:
return str(exc), 400
async with current_app.db_sessionmaker() as db:
async with db.begin():
result = await db.execute(
select(AnsibleGroup)
.where(AnsibleGroup.id == group_id)
.options(selectinload(AnsibleGroup.targets))
)
group = result.scalar_one_or_none()
if group is None:
return "Group not found", 404
name = form.get("name", group.name).strip()
if name:
group.name = name
group.ansible_vars = ansible_vars
new_ids = set(form.getlist("target_ids"))
all_targets = (
await db.execute(select(AnsibleTarget))
).scalars().all()
target_map = {t.id: t for t in all_targets}
group.targets = [target_map[tid] for tid in new_ids if tid in target_map]
return redirect(url_for("ansible_inventory.group_detail", group_id=group_id))
@inventory_bp.post("/groups/<group_id>/delete")
@require_role(UserRole.operator)
async def group_delete(group_id: str):
async with current_app.db_sessionmaker() as db:
async with db.begin():
result = await db.execute(
select(AnsibleGroup).where(AnsibleGroup.id == group_id)
)
group = result.scalar_one_or_none()
if group is not None:
await db.delete(group)
return redirect(url_for("ansible_inventory.groups_list"))
# ---------------------------------------------------------------------------
# Targets
# ---------------------------------------------------------------------------
@inventory_bp.get("/targets")
@require_role(UserRole.viewer)
async def targets_list():
async with current_app.db_sessionmaker() as db:
result = await db.execute(
select(AnsibleTarget)
.options(selectinload(AnsibleTarget.groups))
.order_by(AnsibleTarget.name)
)
targets = result.scalars().all()
return await render_template("ansible/inventory/targets.html", targets=targets)
@inventory_bp.post("/targets")
@require_role(UserRole.operator)
async def targets_create():
form = await request.form
name = form.get("name", "").strip()
address = form.get("address", "").strip()
if not name or not address:
return "name and address are required", 400
try:
ansible_vars = _parse_ansible_vars(form.get("ansible_vars", ""))
except ValueError as exc:
return str(exc), 400
target = AnsibleTarget(
id=str(uuid.uuid4()), name=name, address=address, ansible_vars=ansible_vars
)
async with current_app.db_sessionmaker() as db:
async with db.begin():
db.add(target)
return redirect(url_for("ansible_inventory.targets_list"))
@inventory_bp.get("/targets/<target_id>")
@require_role(UserRole.viewer)
async def target_detail(target_id: str):
async with current_app.db_sessionmaker() as db:
result = await db.execute(
select(AnsibleTarget)
.where(AnsibleTarget.id == target_id)
.options(selectinload(AnsibleTarget.groups))
)
target = result.scalar_one_or_none()
if target is None:
return "Target not found", 404
all_groups = (
await db.execute(select(AnsibleGroup).order_by(AnsibleGroup.name))
).scalars().all()
member_group_ids = {g.id for g in target.groups}
ansible_vars_yaml = yaml.dump(target.ansible_vars) if target.ansible_vars else ""
return await render_template(
"ansible/inventory/target_detail.html",
target=target,
all_groups=all_groups,
member_group_ids=member_group_ids,
ansible_vars_yaml=ansible_vars_yaml,
)
@inventory_bp.post("/targets/<target_id>")
@require_role(UserRole.operator)
async def target_update(target_id: str):
form = await request.form
try:
ansible_vars = _parse_ansible_vars(form.get("ansible_vars", ""))
except ValueError as exc:
return str(exc), 400
async with current_app.db_sessionmaker() as db:
async with db.begin():
result = await db.execute(
select(AnsibleTarget)
.where(AnsibleTarget.id == target_id)
.options(selectinload(AnsibleTarget.groups))
)
target = result.scalar_one_or_none()
if target is None:
return "Target not found", 404
name = form.get("name", target.name).strip()
address = form.get("address", target.address).strip()
if name:
target.name = name
if address:
target.address = address
target.ansible_vars = ansible_vars
new_group_ids = set(form.getlist("group_ids"))
all_groups_result = await db.execute(select(AnsibleGroup))
group_map = {g.id: g for g in all_groups_result.scalars().all()}
target.groups = [group_map[gid] for gid in new_group_ids if gid in group_map]
return redirect(url_for("ansible_inventory.target_detail", target_id=target_id))
@inventory_bp.post("/targets/<target_id>/delete")
@require_role(UserRole.operator)
async def target_delete(target_id: str):
async with current_app.db_sessionmaker() as db:
async with db.begin():
result = await db.execute(
select(AnsibleTarget).where(AnsibleTarget.id == target_id)
)
target = result.scalar_one_or_none()
if target is not None:
await db.delete(target)
return redirect(url_for("ansible_inventory.targets_list"))