import ipaddress from types import SimpleNamespace from fastapi import HTTPException from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.request_context import request_meta from app.models import Address, AuditLog, Base MAX_CAPACITY = 2**53 - 1 SYSTEM = SimpleNamespace(username="system") ANONYMOUS = SimpleNamespace(username="anonymous") _NOUNS = { "organization": ("Организация", "f"), "vrf": ("VRF", "m"), "prefix": ("Префикс", "m"), "address": ("Адрес", "m"), "device": ("Устройство", "n"), "device_type": ("Тип устройства", "m"), "isp": ("Оператор", "m"), } _VERBS = { "created": ("создан", "создана", "создано"), "updated": ("изменён", "изменена", "изменено"), "deleted": ("удалён", "удалена", "удалено"), "assigned": ("назначен", "назначена", "назначено"), } def make_message(entity_type: str, action: str, label: str, diff: dict | None = None) -> str: noun, gender = _NOUNS.get(entity_type, (entity_type, "m")) verb = _VERBS.get(action, (action,) * 3)["mfn".index(gender)] text = f"{noun} {label} {verb}" if action == "updated" and diff: text += ": " + ", ".join(diff) return text def actor_of(username: str) -> str: """Актор для журнала: служебные — как есть, пользователи UI — с префиксом ui:.""" return username if username in ("system", "anonymous") else f"ui:{username}" def audit(db: Session, user, entity_type: str, entity, action: str, label: str, diff: dict | None = None, message: str | None = None): ctx = None if user is SYSTEM else request_meta.get() # системные события (ротация) — без IP, даже если запущены из запроса db.add(AuditLog( username=user.username, entity_type=entity_type, entity_id=getattr(entity, "id", None), entity_label=label, action=action, diff=diff, message=message or make_message(entity_type, action, label, diff), client_ip=ctx["client_ip"] if ctx else None, meta=ctx["meta"] if ctx else None, )) def commit(db: Session, conflict_msg: str = "Запись с такими значениями уже существует"): try: db.commit() except IntegrityError: db.rollback() raise HTTPException(409, conflict_msg) def flush(db: Session, conflict_msg: str = "Запись с такими значениями уже существует"): """flush с тем же переводом нарушений уникальности в 409, что и commit.""" try: db.flush() except IntegrityError: db.rollback() raise HTTPException(409, conflict_msg) def get_or_404(db: Session, model: type[Base], id_: int, what: str = "Объект"): obj = db.get(model, id_) if obj is None: raise HTTPException(404, f"{what} не найден") return obj def capacity(prefix: str) -> int: net = ipaddress.ip_network(prefix) n = net.num_addresses if net.version == 4 and net.prefixlen <= 30: n -= 2 # сеть и broadcast return min(n, MAX_CAPACITY) def utilization(used: int, cap: int) -> int: return round(used * 100 / cap) if cap else 0 def apply_update(obj, data: dict) -> dict: changed = {} for k, v in data.items(): if getattr(obj, k) != v: changed[k] = v setattr(obj, k, v) return changed def count(db: Session, stmt) -> int: return db.scalar(select(func.count()).select_from(stmt.subquery())) or 0 def next_free(db: Session, prefix_id: int, prefix: str) -> str | None: net = ipaddress.ip_network(prefix) used = {ipaddress.ip_address(a) for a in db.scalars(select(Address.address).where(Address.prefix_id == prefix_id))} hosts = net.hosts() if net.version == 4 and net.prefixlen <= 30 else iter(net) for ip in hosts: if ip not in used: return str(ip) return None