eb063aec20
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL. - Доработки дашборда, аддонов, журнала, стилей и API-документации. - Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""Чтение журналов: задания (jobs_history), развёртывание (provision_log), Helm-аддоны (helm_addon_log).
|
|
|
|
Автор: Сергей Антропов
|
|
Сайт: https://devops.org.ru
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from core.cluster_lifecycle import validate_cluster_name
|
|
from core.job_journal import collect_recent_journal_entries_page_sync, read_cluster_journal_sync
|
|
from core.provision_log import (
|
|
HELM_ADDON_LOG_FILENAME,
|
|
PROVISION_LOG_FILENAME,
|
|
collect_cluster_dir_logs_page_sync,
|
|
)
|
|
from models.schemas import (
|
|
ClusterDirLogEntryModel,
|
|
ClusterJournalResponse,
|
|
JournalEntryModel,
|
|
JournalPagedDirLogsResponse,
|
|
JournalRecentResponse,
|
|
)
|
|
|
|
logger = logging.getLogger("kind_k8s.api.journal")
|
|
|
|
router = APIRouter(tags=["journal"])
|
|
|
|
|
|
def _journal_total_pages(total: int, limit: int) -> int:
|
|
if total <= 0 or limit <= 0:
|
|
return 0
|
|
return (total + limit - 1) // limit
|
|
|
|
|
|
def _build_paged_dir_response(
|
|
rows: list[dict],
|
|
*,
|
|
limit: int,
|
|
offset: int,
|
|
total: int,
|
|
) -> JournalPagedDirLogsResponse:
|
|
total_pages = _journal_total_pages(total, limit)
|
|
if total <= 0:
|
|
page = 1
|
|
else:
|
|
page = min((offset // limit) + 1, total_pages)
|
|
page = max(1, page)
|
|
entries = [ClusterDirLogEntryModel.model_validate(r) for r in rows]
|
|
return JournalPagedDirLogsResponse(
|
|
limit=limit,
|
|
offset=offset,
|
|
total=total,
|
|
page=page,
|
|
total_pages=total_pages,
|
|
entries=entries,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/journal/recent",
|
|
response_model=JournalRecentResponse,
|
|
summary="Страница журнала заданий (jobs_history), опционально один кластер",
|
|
)
|
|
async def get_journal_recent(
|
|
limit: int = Query(default=30, ge=1, le=100, description="Записей на страницу"),
|
|
offset: int = Query(default=0, ge=0, description="Смещение (0 = первая страница)"),
|
|
cluster: str | None = Query(
|
|
default=None,
|
|
description="Имя кластера: только записи из clusters/<имя>/journal/jobs_history.json",
|
|
),
|
|
) -> JournalRecentResponse:
|
|
"""Сборка из ``journal/jobs_history.json``; при ``cluster`` — только выбранный каталог."""
|
|
c = cluster.strip() if cluster else None
|
|
if c and not validate_cluster_name(c):
|
|
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
|
|
rows, total = await asyncio.to_thread(
|
|
collect_recent_journal_entries_page_sync,
|
|
limit=limit,
|
|
offset=offset,
|
|
cluster_name=c,
|
|
)
|
|
entries = [JournalEntryModel.model_validate(r) for r in rows]
|
|
total_pages = _journal_total_pages(total, limit)
|
|
if total <= 0:
|
|
page = 1
|
|
else:
|
|
page = min((offset // limit) + 1, total_pages)
|
|
page = max(1, page)
|
|
return JournalRecentResponse(
|
|
limit=limit,
|
|
offset=offset,
|
|
total=total,
|
|
page=page,
|
|
total_pages=total_pages,
|
|
entries=entries,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/journal/provision",
|
|
response_model=JournalPagedDirLogsResponse,
|
|
summary="Журналы развёртывания по кластерам (provision_log.json)",
|
|
)
|
|
async def get_journal_provision_logs(
|
|
limit: int = Query(default=30, ge=1, le=100),
|
|
offset: int = Query(default=0, ge=0),
|
|
) -> JournalPagedDirLogsResponse:
|
|
"""По одному последнему ``provision_log.json`` на кластер, новые первыми."""
|
|
rows, total = await asyncio.to_thread(
|
|
collect_cluster_dir_logs_page_sync,
|
|
filename=PROVISION_LOG_FILENAME,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total)
|
|
|
|
|
|
@router.get(
|
|
"/journal/helm-addons",
|
|
response_model=JournalPagedDirLogsResponse,
|
|
summary="Журналы установки Helm-аддонов (helm_addon_log.json)",
|
|
)
|
|
async def get_journal_helm_addon_logs(
|
|
limit: int = Query(default=30, ge=1, le=100),
|
|
offset: int = Query(default=0, ge=0),
|
|
) -> JournalPagedDirLogsResponse:
|
|
"""Все записи из ``helm_addon_log.json`` по кластерам (история операций), новые первыми."""
|
|
rows, total = await asyncio.to_thread(
|
|
collect_cluster_dir_logs_page_sync,
|
|
filename=HELM_ADDON_LOG_FILENAME,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total)
|
|
|
|
|
|
@router.get(
|
|
"/clusters/{name}/journal",
|
|
response_model=ClusterJournalResponse,
|
|
summary="Журнал заданий кластера (JSON с диска)",
|
|
)
|
|
async def get_cluster_journal(name: str) -> ClusterJournalResponse:
|
|
n = name.strip()
|
|
if not validate_cluster_name(n):
|
|
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
|
|
raw = await asyncio.to_thread(read_cluster_journal_sync, n)
|
|
if raw is None:
|
|
return ClusterJournalResponse(cluster_name=n, file_version=None, entries=[])
|
|
fv = raw.get("file_version") if isinstance(raw.get("file_version"), int) else None
|
|
ent = raw.get("entries")
|
|
if not isinstance(ent, list):
|
|
return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=[])
|
|
entries: list[JournalEntryModel] = []
|
|
for item in ent:
|
|
if isinstance(item, dict):
|
|
try:
|
|
entries.append(JournalEntryModel.model_validate(item))
|
|
except Exception:
|
|
logger.debug("Пропуск некорректной записи журнала кластера «%s»", n)
|
|
return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=entries)
|