Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""Планировщик: запуск задач по cron-расписанию внутри приложения."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from croniter import croniter
|
||||
from sqlmodel import select
|
||||
|
||||
from app.db.database import get_session
|
||||
from app.db.models import ScheduledTask, Server
|
||||
|
||||
CHECK_INTERVAL = 30 # секунд между проверками
|
||||
|
||||
_next_run: dict[int, datetime] = {}
|
||||
_loop_task: Optional[asyncio.Task] = None
|
||||
|
||||
|
||||
# ---------- CRUD ----------
|
||||
|
||||
def list_tasks() -> list[ScheduledTask]:
|
||||
with get_session() as s:
|
||||
return list(s.exec(select(ScheduledTask).order_by(ScheduledTask.id)).all())
|
||||
|
||||
|
||||
def add_task(**fields) -> ScheduledTask:
|
||||
with get_session() as s:
|
||||
t = ScheduledTask(**fields)
|
||||
s.add(t)
|
||||
s.commit()
|
||||
s.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
def update_task(task_id: int, **fields) -> None:
|
||||
with get_session() as s:
|
||||
t = s.get(ScheduledTask, task_id)
|
||||
if t:
|
||||
for k, v in fields.items():
|
||||
setattr(t, k, v)
|
||||
s.add(t)
|
||||
s.commit()
|
||||
_next_run.pop(task_id, None) # пересчитать расписание
|
||||
|
||||
|
||||
def delete_task(task_id: int) -> None:
|
||||
with get_session() as s:
|
||||
t = s.get(ScheduledTask, task_id)
|
||||
if t:
|
||||
s.delete(t)
|
||||
s.commit()
|
||||
_next_run.pop(task_id, None)
|
||||
|
||||
|
||||
# ---------- выполнение ----------
|
||||
|
||||
def _target_servers(target: str) -> list[Server]:
|
||||
with get_session() as s:
|
||||
servers = list(s.exec(select(Server)).all())
|
||||
if target == "all":
|
||||
return servers
|
||||
return [srv for srv in servers if target in (srv.tags or "").split(",")
|
||||
or target.strip() in [t.strip() for t in (srv.tags or "").split(",")]]
|
||||
|
||||
|
||||
async def execute_task(task_id: int) -> str:
|
||||
"""Выполнить задачу немедленно. Возвращает статус."""
|
||||
from app.core import servers as servers_core
|
||||
from app.core.group_ops import op_apt_update, op_upgrade, op_shell, run_group
|
||||
from app.core.tasks import tasks as tm
|
||||
|
||||
with get_session() as s:
|
||||
t = s.get(ScheduledTask, task_id)
|
||||
if not t:
|
||||
return "error: задача не найдена"
|
||||
action, target, params, name = t.action, t.target, t.params, t.name
|
||||
t.last_run = datetime.now(timezone.utc)
|
||||
t.last_status = "running"
|
||||
s.add(t)
|
||||
s.commit()
|
||||
|
||||
status = "success"
|
||||
log_lines: list[str] = []
|
||||
try:
|
||||
if action == "cleanup_metrics":
|
||||
from app.db.models import MetricSnapshot
|
||||
from datetime import timedelta
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=7)
|
||||
with get_session() as s:
|
||||
old = s.exec(select(MetricSnapshot).where(MetricSnapshot.ts < cutoff)).all()
|
||||
for m in old:
|
||||
s.delete(m)
|
||||
s.commit()
|
||||
log_lines.append(f"удалено {len(old)} снапшотов")
|
||||
elif action == "report":
|
||||
from app.core import reports
|
||||
path = await reports.generate()
|
||||
log_lines.append(f"отчёт: {path}")
|
||||
else:
|
||||
servers = _target_servers(target)
|
||||
if not servers:
|
||||
raise RuntimeError(f"нет серверов для target='{target}'")
|
||||
if action == "apt_update":
|
||||
op = op_apt_update()
|
||||
elif action == "upgrade":
|
||||
op = op_upgrade(False)
|
||||
elif action == "dist_upgrade":
|
||||
op = op_upgrade(True)
|
||||
elif action == "shell":
|
||||
op = op_shell(params)
|
||||
elif action == "backup":
|
||||
from app.core.backup import run_backup
|
||||
for srv in servers:
|
||||
rec = await run_backup(srv, params or "/etc")
|
||||
log_lines.append(f"{srv.name}: {rec.status}")
|
||||
op = None
|
||||
elif action == "playbook":
|
||||
from app.core import playbooks as pb
|
||||
from app.core.ssh import SSHClient
|
||||
data = pb.load(params)
|
||||
for srv in servers:
|
||||
async with SSHClient(srv) as c:
|
||||
await pb.run_playbook(c, srv, data, log_lines.append)
|
||||
op = None
|
||||
else:
|
||||
raise RuntimeError(f"неизвестное действие: {action}")
|
||||
|
||||
if op is not None:
|
||||
res = await run_group(servers, op)
|
||||
log_lines.append(f"ok={res.ok_count} err={res.err_count}")
|
||||
for r in res.results.values():
|
||||
if r.status == "error":
|
||||
status = "error"
|
||||
log_lines.append(f"{r.name}: {r.log[-300:]}")
|
||||
except Exception as e:
|
||||
status = "error"
|
||||
log_lines.append(f"[ОШИБКА] {e}")
|
||||
|
||||
with get_session() as s:
|
||||
t = s.get(ScheduledTask, task_id)
|
||||
if t:
|
||||
t.last_status = status
|
||||
s.add(t)
|
||||
s.commit()
|
||||
# Telegram-уведомление
|
||||
try:
|
||||
from app.core import notify
|
||||
await notify.task_done(f"планировщик", name, status)
|
||||
except Exception:
|
||||
pass
|
||||
return status
|
||||
|
||||
|
||||
# ---------- цикл ----------
|
||||
|
||||
async def _loop():
|
||||
while True:
|
||||
try:
|
||||
now = datetime.now(timezone.utc)
|
||||
for t in list_tasks():
|
||||
if not t.enabled:
|
||||
continue
|
||||
nxt = _next_run.get(t.id)
|
||||
if nxt is None:
|
||||
nxt = croniter(t.cron, now).get_next(datetime)
|
||||
_next_run[t.id] = nxt
|
||||
if now >= nxt:
|
||||
_next_run[t.id] = croniter(t.cron, now).get_next(datetime)
|
||||
asyncio.ensure_future(execute_task(t.id))
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(CHECK_INTERVAL)
|
||||
|
||||
|
||||
def start() -> None:
|
||||
global _loop_task
|
||||
if _loop_task is None or _loop_task.done():
|
||||
_loop_task = asyncio.ensure_future(_loop())
|
||||
|
||||
|
||||
# ---------- пресеты ----------
|
||||
|
||||
PRESETS = [
|
||||
("Ежедневный apt update (03:00)", "0 3 * * *", "apt_update", "all", ""),
|
||||
("Еженедельный upgrade (вс 04:00)", "0 4 * * 0", "upgrade", "all", ""),
|
||||
("Ежедневный бэкап /etc (02:00)", "0 2 * * *", "backup", "all", "/etc"),
|
||||
("Бэкап /etc и /home (02:30)", "30 2 * * *", "backup", "all", "/etc /home"),
|
||||
("Очистка старых метрик (ежечасно)", "7 * * * *", "cleanup_metrics", "all", ""),
|
||||
("Ежедневный отчёт (08:00)", "0 8 * * *", "report", "all", ""),
|
||||
("Docker prune (вс 05:00)", "0 5 * * 0", "shell", "all", "docker system prune -f"),
|
||||
("Проверка диска (каждые 6 ч)", "13 */6 * * *", "shell", "all", "df -h"),
|
||||
]
|
||||
Reference in New Issue
Block a user