обновлён /admin/config и API для os_registry. - Molecule/раннер: env из конфигурации, ensure roleforge-os (ensure_roleforge_os.yml), os_registry_pull и доработки executors / runner / create.yml. - /admin/os-images: выбор реестра, buildx (в т.ч. split amd64+arm64 + imagetools), опция --no-cache, стрим логов; domain.py: план команд build, ретраи push. - UI: брендинг (app_name, app_tagline) из app_config через get_ui_branding_context; base.xhtml, role-create / role-view, core.js, pages-main, стили. - Dockerfiles: требование Python ≥3.9 (assert), доработки alt9/astra/debian9/ubuntu20 и др.; новые Dockerfile.arm64 для centos7/centos8. - Конфиг: .env.example, config.py, pyproject.toml.
503 lines
16 KiB
Python
503 lines
16 KiB
Python
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from app.services.ui_branding import get_ui_branding_context
|
|
|
|
router = APIRouter(tags=["ui"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
_PROV_SUBS = ("k3s", "group-all", "group-addons", "group-vault", "inventory", "host-vars", "addons-enable")
|
|
_MOL_SUBS = ("run", "details", "reports")
|
|
|
|
|
|
async def _ctx(request: Request, page: str, **extra: object) -> dict:
|
|
branding = await get_ui_branding_context()
|
|
context = {"request": request, "page": page, **branding}
|
|
context.update(extra)
|
|
return context
|
|
|
|
|
|
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
async def dashboard_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(request=request, name="dashboard.xhtml", context=await _ctx(request, "dashboard"))
|
|
|
|
|
|
@router.get("/clusters", response_class=HTMLResponse)
|
|
async def cluster_list_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="cluster-list.xhtml",
|
|
context=await _ctx(request, "clusters"),
|
|
)
|
|
|
|
|
|
@router.get("/tasks", response_class=HTMLResponse)
|
|
async def tasks_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(request=request, name="tasks.xhtml", context=await _ctx(request, "tasks"))
|
|
|
|
|
|
@router.get("/tasks/{task_id}", response_class=HTMLResponse)
|
|
async def task_logs_page(request: Request, task_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="task-logs.xhtml",
|
|
context=await _ctx(request, "task-logs", task_id=task_id),
|
|
)
|
|
|
|
|
|
@router.get("/tasks/{task_id}/console", response_class=HTMLResponse)
|
|
async def task_console_page(request: Request, task_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="task-console.xhtml",
|
|
context=await _ctx(request, "task-console", task_id=task_id),
|
|
)
|
|
|
|
|
|
@router.get("/molecule", response_class=RedirectResponse)
|
|
async def molecule_index() -> RedirectResponse:
|
|
return RedirectResponse(url="/molecule/run", status_code=302)
|
|
|
|
|
|
@router.get("/molecule/{molecule_sub}", response_class=HTMLResponse)
|
|
async def molecule_page(
|
|
request: Request,
|
|
molecule_sub: str,
|
|
cluster_id: str | None = Query(default=None),
|
|
) -> HTMLResponse:
|
|
if molecule_sub not in _MOL_SUBS:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="error.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"error",
|
|
status_code=404,
|
|
message="Not Found",
|
|
details="Unknown Molecule page",
|
|
),
|
|
status_code=404,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="molecule-tests.xhtml",
|
|
context=await _ctx(request, "molecule", molecule_sub=molecule_sub, cluster_id=cluster_id),
|
|
)
|
|
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(request=request, name="login.xhtml", context=await _ctx(request, "login"))
|
|
|
|
|
|
@router.get("/register", response_class=HTMLResponse)
|
|
async def register_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(request=request, name="register.xhtml", context=await _ctx(request, "register"))
|
|
|
|
|
|
@router.get("/reset-password", response_class=HTMLResponse)
|
|
async def reset_password_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="reset-password.xhtml",
|
|
context=await _ctx(request, "reset-password"),
|
|
)
|
|
|
|
|
|
@router.get("/profile", response_class=HTMLResponse)
|
|
async def profile_me_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="profile-me.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"profile",
|
|
title="My profile",
|
|
description="Your account details and public profile.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/profile/edit", response_class=HTMLResponse)
|
|
async def profile_edit_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="profile-edit.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"profile-edit",
|
|
title="Edit profile",
|
|
description="Update display name, username, and bio.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/profile/avatar", response_class=HTMLResponse)
|
|
async def profile_avatar_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="profile-avatar.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"profile-avatar",
|
|
title="Profile photo",
|
|
description="Upload and manage your profile photo.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/profile/password", response_class=HTMLResponse)
|
|
async def profile_password_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="profile-password.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"profile-password",
|
|
title="Change password",
|
|
description="Update your account password while signed in.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/profile/user/{user_id}", response_class=HTMLResponse)
|
|
async def profile_user_public_page(request: Request, user_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="profile-user.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"profile-user",
|
|
user_id=user_id,
|
|
title="User profile",
|
|
description="Public profile.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/teams", response_class=HTMLResponse)
|
|
async def teams_list_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="teams.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"teams",
|
|
title="Teams",
|
|
description="Create teams, request membership, and share team-scoped Ansible roles.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/teams/{team_id}", response_class=HTMLResponse)
|
|
async def team_detail_page(request: Request, team_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="team-detail.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"team-detail",
|
|
team_id=team_id,
|
|
title="Team",
|
|
description="Members, join requests, invitations, and team roles.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/error", response_class=HTMLResponse)
|
|
async def error_page(request: Request, status_code: int = 500, message: str = "Error") -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="error.xhtml",
|
|
context=await _ctx(request, "error", status_code=status_code, message=message, details=""),
|
|
status_code=status_code,
|
|
)
|
|
|
|
|
|
@router.get("/roles", response_class=HTMLResponse)
|
|
async def roles_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="roles.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"roles",
|
|
title="Roles",
|
|
description="Role catalog grouped by categories with quick import and management.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/roles/create", response_class=HTMLResponse)
|
|
async def role_create_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="role-create.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"roles-create",
|
|
title="Create Role",
|
|
description="Create a role and its catalog; after save you open Role details.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/roles/view/{role_id}", response_class=HTMLResponse)
|
|
async def role_view_page(request: Request, role_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="role-view.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"roles-view",
|
|
role_id=role_id,
|
|
title="Role Details",
|
|
description="View and edit role files, then run Molecule tests.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/inventories", response_class=HTMLResponse)
|
|
async def inventories_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"inventories",
|
|
title="Inventories",
|
|
description="Manage inventory sources and infrastructure templates.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/environment", response_class=HTMLResponse)
|
|
async def environment_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"environment",
|
|
title="Environment",
|
|
description="Define the target environment and baseline context for composition.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/composition", response_class=HTMLResponse)
|
|
async def composition_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"composition",
|
|
title="Composition",
|
|
description="Compose inventories, roles, and reusable building blocks.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/flow", response_class=HTMLResponse)
|
|
async def flow_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"flow",
|
|
title="Flow",
|
|
description="Define orchestration flow and transition rules between steps.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/pipeline", response_class=HTMLResponse)
|
|
async def pipeline_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"pipeline",
|
|
title="Pipeline",
|
|
description="Package flow into a repeatable pipeline with checks and gates.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/execution", response_class=HTMLResponse)
|
|
async def execution_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"execution",
|
|
title="Execution",
|
|
description="Run the full system and observe runtime progress and outcomes.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/playbooks", response_class=HTMLResponse)
|
|
async def playbooks_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"playbooks",
|
|
title="Playbooks",
|
|
description="Build, store, and import or export playbooks as YAML.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/jobs", response_class=HTMLResponse)
|
|
async def jobs_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"jobs",
|
|
title="Jobs",
|
|
description="Run playbooks and track job execution.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/tests", response_class=HTMLResponse)
|
|
async def tests_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"tests",
|
|
title="Molecule Tests",
|
|
description="Test roles and playbooks in a dynamic runtime.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/runners", response_class=HTMLResponse)
|
|
async def runners_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="workspace.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"runners",
|
|
title="Runners",
|
|
description="Active runner instances, status, and manual controls.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/users", response_class=HTMLResponse)
|
|
async def users_admin_list_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="users.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"users",
|
|
title="Users",
|
|
description="Registered accounts (administrators).",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/users/{user_id}", response_class=HTMLResponse)
|
|
async def user_admin_detail_page(request: Request, user_id: str) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="user-detail.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"user-detail",
|
|
user_id=user_id,
|
|
title="User",
|
|
description="Administrator view of a user account.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/admin/config", response_class=HTMLResponse)
|
|
async def admin_config_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="admin-config.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"admin-config",
|
|
title="Admin Config",
|
|
description="Project-wide settings available to root admin users.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/admin/categories", response_class=HTMLResponse)
|
|
async def admin_categories_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="admin-categories.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"admin-categories",
|
|
title="Admin Categories",
|
|
description="Manage global role categories used across the project.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/admin/os-images", response_class=HTMLResponse)
|
|
async def admin_os_images_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="admin-os-images.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"admin-os-images",
|
|
title="Admin OS Images",
|
|
description="Build all Molecule test OS images from dockerfiles directory.",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/admin/lint-config", include_in_schema=False)
|
|
async def admin_lint_config_redirect() -> RedirectResponse:
|
|
return RedirectResponse(url="/admin/yaml-lint-config", status_code=307)
|
|
|
|
|
|
@router.get("/admin/yaml-lint-config", response_class=HTMLResponse)
|
|
async def admin_yaml_lint_config_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="admin-lint.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"admin-yaml-lint-config",
|
|
title="YamlLint config",
|
|
description="yamllint rules for the role file editor (admin only).",
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/admin/json-lint-config", response_class=HTMLResponse)
|
|
async def admin_json_lint_config_page(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="admin-json-lint.xhtml",
|
|
context=await _ctx(
|
|
request,
|
|
"admin-json-lint-config",
|
|
title="JSONLint config",
|
|
description="JSON lint rules for the role file editor (admin only).",
|
|
),
|
|
)
|