mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Keep public facades stable while moving rules/apply/slices and policy/cycles/snapshots into focused modules. Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
"""Config sync snapshot list/detail/export/history."""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import re
|
|
import zipfile
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .config_sync_codec import decompress_text
|
|
from .config_sync_schemas import (
|
|
NeConfigHistoryOut,
|
|
NeConfigSnapshotDetailOut,
|
|
NeConfigSnapshotMetaOut,
|
|
)
|
|
from .models import NeConfigHistory, NeConfigSnapshot
|
|
|
|
def _snap_meta(row: NeConfigSnapshot) -> NeConfigSnapshotMetaOut:
|
|
cmds = row.commands_json if isinstance(row.commands_json, list) else []
|
|
return NeConfigSnapshotMetaOut(
|
|
source=str(row.source),
|
|
target_id=str(row.target_id),
|
|
vendor=str(row.vendor or ""),
|
|
device_type=str(row.device_type or ""),
|
|
ne_name=str(row.ne_name or ""),
|
|
ne_ip=str(row.ne_ip or ""),
|
|
config_sha256=str(row.config_sha256 or ""),
|
|
config_alt_sha256=str(row.config_alt_sha256 or ""),
|
|
plain_size=int(row.plain_size or 0),
|
|
plain_alt_size=int(row.plain_alt_size or 0),
|
|
zlib_size=int(row.zlib_size or 0),
|
|
zlib_alt_size=int(row.zlib_alt_size or 0),
|
|
has_alt=bool(row.config_alt_zlib),
|
|
commands=[str(c) for c in cmds],
|
|
collected_at=row.collected_at,
|
|
last_cycle_id=str(row.last_cycle_id or ""),
|
|
)
|
|
|
|
|
|
def list_snapshots(
|
|
db: Session,
|
|
*,
|
|
page: int,
|
|
page_size: int,
|
|
keyword: str = "",
|
|
source: str = "",
|
|
vendor: str = "",
|
|
) -> dict[str, Any]:
|
|
q = db.query(NeConfigSnapshot)
|
|
src = str(source or "").strip().lower()
|
|
if src in ("managed", "ume"):
|
|
q = q.filter(NeConfigSnapshot.source == src)
|
|
vend = str(vendor or "").strip()
|
|
if vend:
|
|
q = q.filter(NeConfigSnapshot.vendor.ilike(f"%{vend}%"))
|
|
kw = str(keyword or "").strip()
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
q = q.filter(
|
|
or_(
|
|
NeConfigSnapshot.ne_name.ilike(like),
|
|
NeConfigSnapshot.ne_ip.ilike(like),
|
|
NeConfigSnapshot.target_id.ilike(like),
|
|
)
|
|
)
|
|
total = int(q.count())
|
|
rows = q.order_by(NeConfigSnapshot.collected_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
|
return {"total": total, "page": page, "page_size": page_size, "items": [_snap_meta(r) for r in rows]}
|
|
|
|
|
|
def get_snapshot_detail(
|
|
db: Session,
|
|
source: str,
|
|
target_id: str,
|
|
*,
|
|
field: str = "both",
|
|
) -> NeConfigSnapshotDetailOut:
|
|
src = str(source or "").strip().lower()
|
|
tid = str(target_id or "").strip()
|
|
row = db.get(NeConfigSnapshot, {"source": src, "target_id": tid})
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="snapshot_not_found")
|
|
meta = _snap_meta(row)
|
|
primary = ""
|
|
alt = ""
|
|
f = str(field or "both").strip().lower()
|
|
if f in ("primary", "both", ""):
|
|
primary = decompress_text(row.config_zlib)
|
|
if f in ("alt", "both") and row.config_alt_zlib:
|
|
alt = decompress_text(row.config_alt_zlib)
|
|
return NeConfigSnapshotDetailOut(**meta.model_dump(), config_text=primary, config_alt_text=alt)
|
|
|
|
|
|
def _safe_export_part(text: str) -> str:
|
|
s = re.sub(r'[<>:"/\\|?*\s]+', "_", str(text or "").strip())
|
|
return (s[:80] or "ne").strip("._") or "ne"
|
|
|
|
|
|
def build_snapshot_export(
|
|
db: Session,
|
|
source: str,
|
|
target_id: str,
|
|
*,
|
|
field: str = "primary",
|
|
) -> tuple[str, bytes, str]:
|
|
"""Return (filename, payload, media_type) for download."""
|
|
detail = get_snapshot_detail(db, source, target_id, field="both")
|
|
name = _safe_export_part(detail.ne_name or detail.target_id)
|
|
ip = _safe_export_part(detail.ne_ip or "ip")
|
|
base = f"{name}-{ip}-{detail.source}"
|
|
f = str(field or "primary").strip().lower()
|
|
|
|
if f == "alt":
|
|
if not detail.has_alt or not detail.config_alt_text:
|
|
raise HTTPException(status_code=404, detail="alt_config_not_found")
|
|
filename = f"{base}-hierarchical.txt"
|
|
return filename, detail.config_alt_text.encode("utf-8"), "text/plain; charset=utf-8"
|
|
|
|
if f == "both" and detail.has_alt and detail.config_alt_text:
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
zf.writestr(f"{base}-set.txt", detail.config_text or "")
|
|
zf.writestr(f"{base}-hierarchical.txt", detail.config_alt_text or "")
|
|
return f"{base}-configs.zip", buf.getvalue(), "application/zip"
|
|
|
|
filename = f"{base}-config.txt"
|
|
return filename, (detail.config_text or "").encode("utf-8"), "text/plain; charset=utf-8"
|
|
|
|
|
|
def list_snapshot_history(
|
|
db: Session,
|
|
source: str,
|
|
target_id: str,
|
|
*,
|
|
page: int,
|
|
page_size: int,
|
|
) -> dict[str, Any]:
|
|
src = str(source or "").strip().lower()
|
|
tid = str(target_id or "").strip()
|
|
q = (
|
|
db.query(NeConfigHistory)
|
|
.filter(NeConfigHistory.source == src, NeConfigHistory.target_id == tid)
|
|
.order_by(NeConfigHistory.collected_at.desc())
|
|
)
|
|
total = int(q.count())
|
|
rows = q.offset((page - 1) * page_size).limit(page_size).all()
|
|
items: list[NeConfigHistoryOut] = []
|
|
for row in rows:
|
|
cmds = row.commands_json if isinstance(row.commands_json, list) else []
|
|
items.append(
|
|
NeConfigHistoryOut(
|
|
id=str(row.id),
|
|
source=str(row.source),
|
|
target_id=str(row.target_id),
|
|
vendor=str(row.vendor or ""),
|
|
device_type=str(row.device_type or ""),
|
|
ne_name=str(row.ne_name or ""),
|
|
ne_ip=str(row.ne_ip or ""),
|
|
config_sha256=str(row.config_sha256 or ""),
|
|
config_alt_sha256=str(row.config_alt_sha256 or ""),
|
|
plain_size=int(row.plain_size or 0),
|
|
plain_alt_size=int(row.plain_alt_size or 0),
|
|
zlib_size=int(row.zlib_size or 0),
|
|
zlib_alt_size=int(row.zlib_alt_size or 0),
|
|
has_alt=bool(row.config_alt_zlib),
|
|
commands=[str(c) for c in cmds],
|
|
collected_at=row.collected_at,
|
|
last_cycle_id=str(row.cycle_id or ""),
|
|
cycle_id=str(row.cycle_id or ""),
|
|
)
|
|
)
|
|
return {"total": total, "page": page, "page_size": page_size, "items": items}
|
|
|
|
|