298 lines
14 KiB
Python
298 lines
14 KiB
Python
"""Справочники: организации, VRF, операторы, типы устройств, устройства."""
|
|||
|
|
from fastapi import APIRouter, Depends, Query
|
||
|
|
from sqlalchemy import String, cast, func, or_, select
|
||
|
|
from sqlalchemy.orm import Session
|
||
|
|
|
||
|
|
from app import schemas as s
|
||
|
|
from app.db import get_db
|
||
|
|
from app.models import (
|
||
|
|
Address, AddressStatus, Device, DeviceType, Isp, IspNetwork, Organization, Prefix, User, Vrf,
|
||
|
|
)
|
||
|
|
from app.security import admin_user, current_user
|
||
|
|
from app.services import apply_update, audit, commit, count, flush, get_or_404
|
||
|
|
from fastapi import HTTPException
|
||
|
|
|
||
|
|
router = APIRouter(dependencies=[Depends(current_user)])
|
||
|
|
|
||
|
|
|
||
|
|
def _like(q: str) -> str:
|
||
|
|
return f"%{q.strip()}%"
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- organizations
|
||
|
|
def _org_out(db: Session, o: Organization) -> s.OrgOut:
|
||
|
|
out = s.OrgOut.model_validate(o)
|
||
|
|
out.prefixes_count = count(db, select(Prefix.id).where(Prefix.organization_id == o.id))
|
||
|
|
out.addresses_count = count(
|
||
|
|
db, select(Address.id).join(Prefix).where(Prefix.organization_id == o.id, Address.status == AddressStatus.assigned)
|
||
|
|
)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/organizations", response_model=s.Page[s.OrgOut], tags=["organizations"])
|
||
|
|
def list_orgs(q: str = "", limit: int = Query(100, le=500), offset: int = 0, db: Session = Depends(get_db)):
|
||
|
|
stmt = select(Organization)
|
||
|
|
if q:
|
||
|
|
stmt = stmt.where(or_(*(c.ilike(_like(q)) for c in (Organization.name, Organization.short_name, Organization.inn, Organization.address))))
|
||
|
|
total = count(db, stmt)
|
||
|
|
rows = db.scalars(stmt.order_by(Organization.id).limit(limit).offset(offset)).all()
|
||
|
|
return s.Page(items=[_org_out(db, o) for o in rows], total=total)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/organizations/{id}", response_model=s.OrgOut, tags=["organizations"])
|
||
|
|
def get_org(id: int, db: Session = Depends(get_db)):
|
||
|
|
return _org_out(db, get_or_404(db, Organization, id, "Организация"))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/organizations", response_model=s.OrgOut, status_code=201, tags=["organizations"])
|
||
|
|
def create_org(body: s.OrgIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
o = Organization(**body.model_dump())
|
||
|
|
db.add(o)
|
||
|
|
flush(db, "Организация с таким названием или ИНН уже существует")
|
||
|
|
db.add(Vrf(organization_id=o.id, name="default"))
|
||
|
|
audit(db, user, "organization", o, "created", o.name)
|
||
|
|
commit(db, "Организация с таким названием или ИНН уже существует")
|
||
|
|
return _org_out(db, o)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/organizations/{id}", response_model=s.OrgOut, tags=["organizations"])
|
||
|
|
def update_org(id: int, body: s.OrgIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
o = get_or_404(db, Organization, id, "Организация")
|
||
|
|
changed = apply_update(o, body.model_dump())
|
||
|
|
audit(db, user, "organization", o, "updated", o.name, changed)
|
||
|
|
commit(db, "Организация с таким названием или ИНН уже существует")
|
||
|
|
return _org_out(db, o)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/organizations/{id}", status_code=204, tags=["organizations"])
|
||
|
|
def delete_org(id: int, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
o = get_or_404(db, Organization, id, "Организация")
|
||
|
|
busy = count(db, select(Prefix.id).where(Prefix.organization_id == id)) or count(
|
||
|
|
db, select(Device.id).where(Device.organization_id == id)) or count(db, select(Isp.id).where(Isp.organization_id == id))
|
||
|
|
if busy:
|
||
|
|
raise HTTPException(409, "Нельзя удалить: у организации есть префиксы, устройства или операторы")
|
||
|
|
for v in db.scalars(select(Vrf).where(Vrf.organization_id == id)):
|
||
|
|
db.delete(v)
|
||
|
|
audit(db, user, "organization", o, "deleted", o.name)
|
||
|
|
db.delete(o)
|
||
|
|
commit(db)
|
||
|
|
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------- VRF
|
||
|
|
def _vrf_out(db: Session, v: Vrf) -> s.VrfOut:
|
||
|
|
out = s.VrfOut.model_validate(v)
|
||
|
|
out.prefixes_count = count(db, select(Prefix.id).where(Prefix.vrf_id == v.id))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/vrfs", response_model=s.Page[s.VrfOut], tags=["vrf"])
|
||
|
|
def list_vrfs(organization_id: int | None = None, db: Session = Depends(get_db)):
|
||
|
|
stmt = select(Vrf)
|
||
|
|
if organization_id:
|
||
|
|
stmt = stmt.where(Vrf.organization_id == organization_id)
|
||
|
|
rows = db.scalars(stmt.order_by(Vrf.id)).all()
|
||
|
|
return s.Page(items=[_vrf_out(db, v) for v in rows], total=len(rows))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/vrfs", response_model=s.VrfOut, status_code=201, tags=["vrf"])
|
||
|
|
def create_vrf(body: s.VrfIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
get_or_404(db, Organization, body.organization_id, "Организация")
|
||
|
|
v = Vrf(**body.model_dump())
|
||
|
|
db.add(v)
|
||
|
|
flush(db, "VRF с таким названием уже есть в организации")
|
||
|
|
audit(db, user, "vrf", v, "created", v.name)
|
||
|
|
commit(db, "VRF с таким названием уже есть в организации")
|
||
|
|
return _vrf_out(db, v)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/vrfs/{id}", response_model=s.VrfOut, tags=["vrf"])
|
||
|
|
def update_vrf(id: int, body: s.VrfUpdate, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
v = get_or_404(db, Vrf, id, "VRF")
|
||
|
|
changed = apply_update(v, body.model_dump(exclude_unset=True, exclude_none=True))
|
||
|
|
audit(db, user, "vrf", v, "updated", v.name, changed)
|
||
|
|
commit(db, "VRF с таким названием уже есть в организации")
|
||
|
|
return _vrf_out(db, v)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/vrfs/{id}", status_code=204, tags=["vrf"])
|
||
|
|
def delete_vrf(id: int, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
v = get_or_404(db, Vrf, id, "VRF")
|
||
|
|
if count(db, select(Prefix.id).where(Prefix.vrf_id == id)):
|
||
|
|
raise HTTPException(409, "Нельзя удалить: VRF используется префиксами")
|
||
|
|
audit(db, user, "vrf", v, "deleted", v.name)
|
||
|
|
db.delete(v)
|
||
|
|
commit(db)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- device types
|
||
|
|
def _type_out(db: Session, t: DeviceType) -> s.DeviceTypeOut:
|
||
|
|
out = s.DeviceTypeOut.model_validate(t)
|
||
|
|
out.devices_count = count(db, select(Device.id).where(Device.device_type_id == t.id))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/device-types", response_model=s.Page[s.DeviceTypeOut], tags=["devices"])
|
||
|
|
def list_types(db: Session = Depends(get_db)):
|
||
|
|
rows = db.scalars(select(DeviceType).order_by(DeviceType.id)).all()
|
||
|
|
return s.Page(items=[_type_out(db, t) for t in rows], total=len(rows))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/device-types", response_model=s.DeviceTypeOut, status_code=201, tags=["devices"])
|
||
|
|
def create_type(body: s.DeviceTypeIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
t = DeviceType(name=body.name)
|
||
|
|
db.add(t)
|
||
|
|
flush(db, "Тип с таким названием уже существует")
|
||
|
|
audit(db, user, "device_type", t, "created", t.name)
|
||
|
|
commit(db, "Тип с таким названием уже существует")
|
||
|
|
return _type_out(db, t)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/device-types/{id}", response_model=s.DeviceTypeOut, tags=["devices"])
|
||
|
|
def update_type(id: int, body: s.DeviceTypeIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
t = get_or_404(db, DeviceType, id, "Тип")
|
||
|
|
changed = apply_update(t, {"name": body.name})
|
||
|
|
audit(db, user, "device_type", t, "updated", t.name, changed)
|
||
|
|
commit(db, "Тип с таким названием уже существует")
|
||
|
|
return _type_out(db, t)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/device-types/{id}", status_code=204, tags=["devices"])
|
||
|
|
def delete_type(id: int, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
t = get_or_404(db, DeviceType, id, "Тип")
|
||
|
|
if t.is_default:
|
||
|
|
raise HTTPException(409, "Нельзя удалить тип по умолчанию")
|
||
|
|
if count(db, select(Device.id).where(Device.device_type_id == id)):
|
||
|
|
raise HTTPException(409, "Нельзя удалить: тип используется устройствами")
|
||
|
|
audit(db, user, "device_type", t, "deleted", t.name)
|
||
|
|
db.delete(t)
|
||
|
|
commit(db)
|
||
|
|
|
||
|
|
|
||
|
|
# --------------------------------------------------------------------- devices
|
||
|
|
def _device_out(db: Session, d: Device) -> s.DeviceOut:
|
||
|
|
rows = db.execute(
|
||
|
|
select(Address.address, Address.prefix_id, Address.status).where(Address.device_id == d.id).order_by(Address.address)
|
||
|
|
).all()
|
||
|
|
t = db.get(DeviceType, d.device_type_id)
|
||
|
|
return s.DeviceOut(
|
||
|
|
id=d.id, name=d.name, device_type_id=d.device_type_id, device_type_name=t.name,
|
||
|
|
organization_id=d.organization_id, mac=d.mac, note=d.note,
|
||
|
|
ip_addresses=[s.ip_text(r[0]) for r in rows], first_prefix_id=rows[0][1] if rows else None,
|
||
|
|
all_deprecated=bool(rows) and all(r[2] == AddressStatus.deprecated for r in rows),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/devices", response_model=s.Page[s.DeviceOut], tags=["devices"])
|
||
|
|
def list_devices(
|
||
|
|
organization_id: int | None = None, device_type_id: int | None = None, q: str = "",
|
||
|
|
limit: int = Query(100, le=500), offset: int = 0, db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
stmt = select(Device)
|
||
|
|
if organization_id:
|
||
|
|
stmt = stmt.where(Device.organization_id == organization_id)
|
||
|
|
if device_type_id:
|
||
|
|
stmt = stmt.where(Device.device_type_id == device_type_id)
|
||
|
|
if q:
|
||
|
|
ip_match = select(Address.device_id).where(func.host(Address.address).ilike(_like(q)))
|
||
|
|
stmt = stmt.where(or_(Device.name.ilike(_like(q)), Device.note.ilike(_like(q)), Device.id.in_(ip_match)))
|
||
|
|
total = count(db, stmt)
|
||
|
|
rows = db.scalars(stmt.order_by(Device.id).limit(limit).offset(offset)).all()
|
||
|
|
return s.Page(items=[_device_out(db, d) for d in rows], total=total)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/devices", response_model=s.DeviceOut, status_code=201, tags=["devices"])
|
||
|
|
def create_device(body: s.DeviceIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
get_or_404(db, Organization, body.organization_id, "Организация")
|
||
|
|
get_or_404(db, DeviceType, body.device_type_id, "Тип")
|
||
|
|
d = Device(**body.model_dump())
|
||
|
|
db.add(d)
|
||
|
|
flush(db, "Устройство с таким именем уже есть в организации")
|
||
|
|
audit(db, user, "device", d, "created", d.name)
|
||
|
|
commit(db, "Устройство с таким именем уже есть в организации")
|
||
|
|
return _device_out(db, d)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/devices/{id}", response_model=s.DeviceOut, tags=["devices"])
|
||
|
|
def get_device(id: int, db: Session = Depends(get_db)):
|
||
|
|
return _device_out(db, get_or_404(db, Device, id, "Устройство"))
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/devices/{id}", response_model=s.DeviceOut, tags=["devices"])
|
||
|
|
def update_device(id: int, body: s.DeviceUpdate, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
d = get_or_404(db, Device, id, "Устройство")
|
||
|
|
data = body.model_dump(exclude_unset=True, exclude_none=True)
|
||
|
|
if "device_type_id" in data:
|
||
|
|
get_or_404(db, DeviceType, data["device_type_id"], "Тип")
|
||
|
|
changed = apply_update(d, data)
|
||
|
|
audit(db, user, "device", d, "updated", d.name, changed)
|
||
|
|
commit(db, "Устройство с таким именем уже есть в организации")
|
||
|
|
return _device_out(db, d)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/devices/{id}", status_code=204, tags=["devices"])
|
||
|
|
def delete_device(id: int, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
d = get_or_404(db, Device, id, "Устройство")
|
||
|
|
audit(db, user, "device", d, "deleted", d.name)
|
||
|
|
db.delete(d)
|
||
|
|
commit(db)
|
||
|
|
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------ ISPs
|
||
|
|
def _isp_out(db: Session, i: Isp) -> s.IspOut:
|
||
|
|
org = db.get(Organization, i.organization_id)
|
||
|
|
return s.IspOut(
|
||
|
|
id=i.id, name=i.name, organization_id=i.organization_id, organization_name=org.name,
|
||
|
|
networks=[str(n.cidr) for n in i.networks], hotline=i.hotline,
|
||
|
|
contract_number=i.contract_number, note=i.note,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/isps", response_model=s.Page[s.IspOut], tags=["isps"])
|
||
|
|
def list_isps(
|
||
|
|
organization_id: int | None = None, q: str = "", limit: int = Query(100, le=500), offset: int = 0,
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
stmt = select(Isp)
|
||
|
|
if organization_id:
|
||
|
|
stmt = stmt.where(Isp.organization_id == organization_id)
|
||
|
|
if q:
|
||
|
|
nets = select(IspNetwork.isp_id).where(cast(IspNetwork.cidr, String).ilike(_like(q)))
|
||
|
|
orgs = select(Organization.id).where(Organization.name.ilike(_like(q)))
|
||
|
|
stmt = stmt.where(or_(Isp.name.ilike(_like(q)), Isp.id.in_(nets), Isp.organization_id.in_(orgs)))
|
||
|
|
total = count(db, stmt)
|
||
|
|
rows = db.scalars(stmt.order_by(Isp.id).limit(limit).offset(offset)).all()
|
||
|
|
return s.Page(items=[_isp_out(db, i) for i in rows], total=total)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/isps", response_model=s.IspOut, status_code=201, tags=["isps"])
|
||
|
|
def create_isp(body: s.IspIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
get_or_404(db, Organization, body.organization_id, "Организация")
|
||
|
|
data = body.model_dump()
|
||
|
|
nets = data.pop("networks")
|
||
|
|
i = Isp(**data, networks=[IspNetwork(cidr=n) for n in nets])
|
||
|
|
db.add(i)
|
||
|
|
db.flush()
|
||
|
|
audit(db, user, "isp", i, "created", i.name)
|
||
|
|
commit(db)
|
||
|
|
return _isp_out(db, i)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/isps/{id}", response_model=s.IspOut, tags=["isps"])
|
||
|
|
def update_isp(id: int, body: s.IspIn, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
i = get_or_404(db, Isp, id, "Оператор")
|
||
|
|
get_or_404(db, Organization, body.organization_id, "Организация")
|
||
|
|
data = body.model_dump()
|
||
|
|
nets = data.pop("networks")
|
||
|
|
changed = apply_update(i, data)
|
||
|
|
i.networks = [IspNetwork(cidr=n) for n in nets]
|
||
|
|
audit(db, user, "isp", i, "updated", i.name, changed)
|
||
|
|
commit(db)
|
||
|
|
return _isp_out(db, i)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/isps/{id}", status_code=204, tags=["isps"])
|
||
|
|
def delete_isp(id: int, db: Session = Depends(get_db), user: User = Depends(admin_user)):
|
||
|
|
i = get_or_404(db, Isp, id, "Оператор")
|
||
|
|
audit(db, user, "isp", i, "deleted", i.name)
|
||
|
|
db.delete(i)
|
||
|
|
commit(db)
|