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,126 @@
|
||||
"""Cron: чтение/запись crontab, версии, описание расписаний."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from croniter import croniter
|
||||
|
||||
from app.core.ssh import SSHClient
|
||||
from app.db.database import get_session
|
||||
from app.db.models import CronBackup
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronLine:
|
||||
raw: str
|
||||
enabled: bool
|
||||
schedule: str
|
||||
command: str
|
||||
comment: str = ""
|
||||
|
||||
@property
|
||||
def is_entry(self) -> bool:
|
||||
return bool(self.schedule)
|
||||
|
||||
|
||||
def parse_crontab(content: str) -> List[CronLine]:
|
||||
lines: List[CronLine] = []
|
||||
for raw in content.splitlines():
|
||||
stripped = raw.strip()
|
||||
enabled = True
|
||||
candidate = stripped
|
||||
if not candidate:
|
||||
lines.append(CronLine(raw, True, "", ""))
|
||||
continue
|
||||
if candidate.startswith("#"):
|
||||
enabled = False
|
||||
candidate = candidate.lstrip("#").strip()
|
||||
parts = candidate.split(None, 5)
|
||||
if candidate.startswith("@") and len(candidate.split(None, 1)) == 2:
|
||||
sched, cmd = candidate.split(None, 1)
|
||||
lines.append(CronLine(raw, enabled, sched, cmd))
|
||||
elif len(parts) == 6 and _looks_like_schedule(parts[:5]):
|
||||
lines.append(CronLine(raw, enabled, " ".join(parts[:5]), parts[5]))
|
||||
else:
|
||||
lines.append(CronLine(raw, enabled, "", "", comment=stripped))
|
||||
return lines
|
||||
|
||||
|
||||
def _looks_like_schedule(fields: List[str]) -> bool:
|
||||
for f in fields:
|
||||
for ch in f:
|
||||
if ch not in "0123456789*,-/":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def humanize(schedule: str) -> str:
|
||||
try:
|
||||
if schedule.startswith("@"):
|
||||
return {"@reboot": "при загрузке", "@daily": "ежедневно",
|
||||
"@hourly": "ежечасно", "@weekly": "еженедельно",
|
||||
"@monthly": "ежемесячно", "@yearly": "ежегодно"}.get(schedule, schedule)
|
||||
it = croniter(schedule)
|
||||
nxt = it.get_next(__import__("datetime").datetime)
|
||||
return f"следующий запуск: {nxt:%Y-%m-%d %H:%M}"
|
||||
except Exception:
|
||||
return "неверное выражение"
|
||||
|
||||
|
||||
def render(lines: List[CronLine]) -> str:
|
||||
out = []
|
||||
for l in lines:
|
||||
if not l.is_entry:
|
||||
out.append(l.raw)
|
||||
continue
|
||||
entry = f"{l.schedule} {l.command}"
|
||||
out.append(entry if l.enabled else f"# {entry}")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
async def get_crontab(c: SSHClient, user: Optional[str] = None) -> str:
|
||||
cmd = f"crontab -u {user} -l" if user else "crontab -l"
|
||||
r = await c.sudo(cmd, timeout=30)
|
||||
if not r.ok and "no crontab" in (r.stderr + r.stdout):
|
||||
return ""
|
||||
return r.stdout
|
||||
|
||||
|
||||
async def set_crontab(c: SSHClient, content: str, server_id: int,
|
||||
user: Optional[str] = None) -> None:
|
||||
# бэкап текущего
|
||||
current = await get_crontab(c, user)
|
||||
with get_session() as s:
|
||||
s.add(CronBackup(server_id=server_id, user=user or "current", content=current))
|
||||
s.commit()
|
||||
import base64
|
||||
b64 = base64.b64encode(content.encode()).decode()
|
||||
target = f"crontab -u {user} -" if user else "crontab -"
|
||||
r = await c.sudo(f"echo {b64} | base64 -d | {target}", timeout=30)
|
||||
if not r.ok:
|
||||
raise RuntimeError(r.stderr)
|
||||
|
||||
|
||||
async def list_cron_files(c: SSHClient) -> dict:
|
||||
"""Системные cron-файлы: /etc/crontab + /etc/cron.d/*"""
|
||||
out = {}
|
||||
r = await c.sudo("cat /etc/crontab 2>/dev/null", timeout=30)
|
||||
if r.ok:
|
||||
out["/etc/crontab"] = r.stdout
|
||||
r = await c.sudo("ls /etc/cron.d/ 2>/dev/null", timeout=30)
|
||||
for name in r.stdout.split():
|
||||
rr = await c.sudo(f"cat '/etc/cron.d/{name}'", timeout=30)
|
||||
if rr.ok:
|
||||
out[f"/etc/cron.d/{name}"] = rr.stdout
|
||||
return out
|
||||
|
||||
|
||||
def backups(server_id: int, user: str, limit: int = 10) -> List[CronBackup]:
|
||||
from sqlmodel import select
|
||||
with get_session() as s:
|
||||
return list(s.exec(
|
||||
select(CronBackup)
|
||||
.where(CronBackup.server_id == server_id, CronBackup.user == user)
|
||||
.order_by(CronBackup.id.desc()).limit(limit)
|
||||
).all())
|
||||
Reference in New Issue
Block a user