first commit

This commit is contained in:
Mateusz Gruszczyński
2026-03-04 15:21:03 +01:00
commit 5429f176c9
53 changed files with 3808 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
# Szkielet do dopisania.
# Zalecane: librouteros (synch) w executorze lub biblioteka async jeśli użyjesz.
from typing import Any, Dict, List
from app.services.mikrotik.client_base import MikroTikClient
class MikroTikAPIClient(MikroTikClient):
def __init__(self, host: str, port: int, username: str, password: str, use_ssl: bool = False):
self.host = host
self.port = port
self.username = username
self.password = password
self.use_ssl = use_ssl
async def list_interfaces(self) -> List[Dict[str, Any]]:
raise NotImplementedError("API list_interfaces not implemented")
async def monitor_traffic_once(self, iface: str) -> Dict[str, Any]:
raise NotImplementedError("API monitor_traffic_once not implemented")

View File

@@ -0,0 +1,11 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, List
class MikroTikClient(ABC):
@abstractmethod
async def list_interfaces(self) -> List[Dict[str, Any]]:
...
@abstractmethod
async def monitor_traffic_once(self, iface: str) -> Dict[str, Any]:
...

View File

@@ -0,0 +1,62 @@
import json
from typing import Any, Dict, List
import httpx
from app.services.mikrotik.client_base import MikroTikClient
def parse_bps(value: Any) -> float:
if value is None:
return 0.0
s = str(value).strip().lower().replace(" ", "")
if s == "":
return 0.0
try:
return float(s)
except ValueError:
pass
multipliers = {"bps": 1.0, "kbps": 1_000.0, "mbps": 1_000_000.0, "gbps": 1_000_000_000.0}
for unit, mul in multipliers.items():
if s.endswith(unit):
num = s[: -len(unit)]
try:
return float(num) * mul
except ValueError:
return 0.0
return 0.0
class MikroTikRESTClient(MikroTikClient):
def __init__(self, base_url: str, username: str, password: str, verify_ssl: bool):
self.base = base_url.rstrip("/") + "/rest"
self.auth = (username, password)
self.verify = verify_ssl
async def _client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(
base_url=self.base,
auth=self.auth,
verify=self.verify,
timeout=httpx.Timeout(10.0),
headers={"Content-Type": "application/json"},
)
async def list_interfaces(self) -> List[Dict[str, Any]]:
params = {".proplist": "name,type,disabled,running"}
async with await self._client() as c:
r = await c.get("/interface", params=params)
r.raise_for_status()
data = r.json()
if isinstance(data, dict):
return [data]
return data
async def monitor_traffic_once(self, iface: str) -> Dict[str, Any]:
payload = {"interface": iface, "once": ""}
async with await self._client() as c:
r = await c.post("/interface/monitor-traffic", content=json.dumps(payload))
r.raise_for_status()
data = r.json()
if isinstance(data, list) and data:
return data[0]
if isinstance(data, dict):
return data
return {}

View File

@@ -0,0 +1,19 @@
# Szkielet do dopisania.
# Zalecane: asyncssh + komenda:
# /interface/monitor-traffic interface=ether1 once
# i parsowanie wyjścia.
from typing import Any, Dict, List
from app.services.mikrotik.client_base import MikroTikClient
class MikroTikSSHClient(MikroTikClient):
def __init__(self, host: str, port: int, username: str, password: str):
self.host = host
self.port = port
self.username = username
self.password = password
async def list_interfaces(self) -> List[Dict[str, Any]]:
raise NotImplementedError("SSH list_interfaces not implemented")
async def monitor_traffic_once(self, iface: str) -> Dict[str, Any]:
raise NotImplementedError("SSH monitor_traffic_once not implemented")

View File

@@ -0,0 +1,54 @@
import json
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import decrypt_secret
from app.models.router import Router, RouterCredential, CredentialMethod, RouterMethod
from app.services.mikrotik.client_rest import MikroTikRESTClient
from app.services.mikrotik.client_ssh import MikroTikSSHClient
from app.services.mikrotik.client_api import MikroTikAPIClient
async def build_client(session: AsyncSession, router_id: int):
rres = await session.execute(select(Router).where(Router.id == router_id))
router = rres.scalar_one_or_none()
if not router:
return None
cres = await session.execute(select(RouterCredential).where(RouterCredential.router_id == router_id))
creds = cres.scalars().all()
# wybór: preferowana metoda albo auto: REST -> API -> SSH
order = []
pref = router.preferred_method
if pref == RouterMethod.REST:
order = [CredentialMethod.REST]
elif pref == RouterMethod.API:
order = [CredentialMethod.API]
elif pref == RouterMethod.SSH:
order = [CredentialMethod.SSH]
else:
order = [CredentialMethod.REST, CredentialMethod.API, CredentialMethod.SSH]
cred_by_method = {c.method: c for c in creds}
for m in order:
c = cred_by_method.get(m)
if not c:
continue
secret = decrypt_secret(c.secret_encrypted)
extra = {}
try:
extra = json.loads(c.extra_json or "{}")
except Exception:
extra = {}
if m == CredentialMethod.REST:
base_url = f"https://{router.host}:{router.port_rest}"
if extra.get("scheme") in ("http", "https"):
base_url = f"{extra['scheme']}://{router.host}:{router.port_rest}"
return MikroTikRESTClient(base_url, c.username, secret, router.verify_ssl)
if m == CredentialMethod.SSH:
return MikroTikSSHClient(router.host, router.port_ssh, c.username, secret)
if m == CredentialMethod.API:
return MikroTikAPIClient(router.host, router.port_api, c.username, secret, use_ssl=bool(extra.get("ssl", False)))
return None