257 lines
8.9 KiB
Python
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"))
|