new features
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user, get_db
|
||||
from app.models.router import Router
|
||||
from app.models.user import User
|
||||
from app.schemas.router import RouterCreate, RouterResponse, RouterTestConnection, RouterUpdate
|
||||
from app.schemas.router import RouterCreate, RouterPingStatus, RouterResponse, RouterTestConnection, RouterUpdate
|
||||
from app.services.router_service import router_service
|
||||
from app.services.settings_service import settings_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RouterPingBulkResponse(BaseModel):
|
||||
items: list[RouterPingStatus]
|
||||
|
||||
|
||||
def serialize_router(router: Router, global_settings) -> RouterResponse:
|
||||
has_router_key = bool((router.ssh_key or '').strip())
|
||||
has_global_key = bool((global_settings.global_ssh_key or '').strip())
|
||||
@@ -49,6 +54,12 @@ def list_routers(current_user: User = Depends(get_current_user), db: Session = D
|
||||
return [serialize_router(router, global_settings) for router in routers]
|
||||
|
||||
|
||||
@router.get('/ping-statuses', response_model=RouterPingBulkResponse)
|
||||
def list_ping_statuses(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
routers = db.query(Router).filter(Router.owner_id == current_user.id).all()
|
||||
return RouterPingBulkResponse(items=router_service.ping_many(routers))
|
||||
|
||||
|
||||
@router.post('', response_model=RouterResponse)
|
||||
def create_router(payload: RouterCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
router_data = payload.model_dump()
|
||||
|
||||
@@ -61,6 +61,10 @@ def _run_lightweight_migrations() -> None:
|
||||
_ensure_column('routers', 'last_connection_auth_mode', 'VARCHAR(64)')
|
||||
_ensure_column('routers', 'last_connection_http_status', 'VARCHAR(32)')
|
||||
_ensure_column('routers', 'last_connection_backup_available', 'BOOLEAN')
|
||||
_ensure_column('routers', 'disable_all_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||
_ensure_column('routers', 'disable_export_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||
_ensure_column('routers', 'disable_binary_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||
_ensure_column('routers', 'disable_ping', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||
|
||||
|
||||
def init_db():
|
||||
|
||||
@@ -29,6 +29,10 @@ class Router(Base):
|
||||
last_connection_auth_mode = Column(String(64), nullable=True)
|
||||
last_connection_http_status = Column(String(32), nullable=True)
|
||||
last_connection_backup_available = Column(Boolean, nullable=True)
|
||||
disable_all_backups = Column(Boolean, nullable=False, default=False)
|
||||
disable_export_backups = Column(Boolean, nullable=False, default=False)
|
||||
disable_binary_backups = Column(Boolean, nullable=False, default=False)
|
||||
disable_ping = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(DateTime, server_default=func.now(), nullable=False)
|
||||
|
||||
backups = relationship("Backup", back_populates="router", cascade="all, delete-orphan")
|
||||
|
||||
@@ -16,6 +16,10 @@ class RouterBase(BaseModel):
|
||||
ssh_user: str | None = Field(default=None, max_length=120)
|
||||
ssh_key: str | None = None
|
||||
ssh_password: str | None = None
|
||||
disable_all_backups: bool = False
|
||||
disable_export_backups: bool = False
|
||||
disable_binary_backups: bool = False
|
||||
disable_ping: bool = False
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
@@ -54,6 +58,10 @@ class RouterUpdate(BaseModel):
|
||||
ssh_user: str | None = None
|
||||
ssh_key: str | None = None
|
||||
ssh_password: str | None = None
|
||||
disable_all_backups: bool | None = None
|
||||
disable_export_backups: bool | None = None
|
||||
disable_binary_backups: bool | None = None
|
||||
disable_ping: bool | None = None
|
||||
|
||||
@field_validator("name", "host", "ssh_user", "ssh_key", "ssh_password", mode="before")
|
||||
@classmethod
|
||||
@@ -72,6 +80,10 @@ class RouterResponse(RouterBase):
|
||||
has_effective_password: bool = False
|
||||
supports_export: bool = False
|
||||
supports_restore_upload: bool = False
|
||||
disable_all_backups: bool = False
|
||||
disable_export_backups: bool = False
|
||||
disable_binary_backups: bool = False
|
||||
disable_ping: bool = False
|
||||
last_connection_status: bool | None = None
|
||||
last_connection_tested_at: datetime | None = None
|
||||
last_connection_error: str | None = None
|
||||
@@ -102,3 +114,10 @@ class RouterTestConnection(BaseModel):
|
||||
auth_mode: str | None = None
|
||||
http_status: str | None = None
|
||||
backup_available: bool | None = None
|
||||
|
||||
|
||||
class RouterPingStatus(BaseModel):
|
||||
router_id: int
|
||||
reachable: bool
|
||||
latency_ms: float | None = None
|
||||
disabled: bool = False
|
||||
|
||||
@@ -191,6 +191,8 @@ class BackupService:
|
||||
router = self._router_for_user(db, user, router_id)
|
||||
if router.device_type != 'routeros':
|
||||
raise HTTPException(status_code=400, detail='Text export is available only for RouterOS devices')
|
||||
if router.disable_all_backups or router.disable_export_backups:
|
||||
raise HTTPException(status_code=400, detail='Exports are disabled for this device')
|
||||
settings = settings_service.get_or_create(db)
|
||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
name = f'{router.name}_{router.id}_{stamp}.rsc'
|
||||
@@ -214,6 +216,8 @@ class BackupService:
|
||||
|
||||
def binary_backup(self, db: Session, user: User, router_id: int) -> Backup:
|
||||
router = self._router_for_user(db, user, router_id)
|
||||
if router.disable_all_backups or router.disable_binary_backups:
|
||||
raise HTTPException(status_code=400, detail='Binary backups are disabled for this device')
|
||||
settings = settings_service.get_or_create(db)
|
||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
base_name = f'{router.name}_{router.id}_{stamp}'
|
||||
@@ -306,6 +310,13 @@ class BackupService:
|
||||
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
||||
result = []
|
||||
for router in routers:
|
||||
if router.disable_all_backups or router.disable_export_backups:
|
||||
result.append({
|
||||
'router': router.name,
|
||||
'status': 'skipped',
|
||||
'message': 'Exports are disabled for this device',
|
||||
})
|
||||
continue
|
||||
if (router.device_type or 'routeros').lower() != 'routeros':
|
||||
result.append({
|
||||
'router': router.name,
|
||||
@@ -324,6 +335,13 @@ class BackupService:
|
||||
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
||||
result = []
|
||||
for router in routers:
|
||||
if router.disable_all_backups or router.disable_binary_backups:
|
||||
result.append({
|
||||
'router': router.name,
|
||||
'status': 'skipped',
|
||||
'message': 'Binary backups are disabled for this device',
|
||||
})
|
||||
continue
|
||||
try:
|
||||
backup = self.binary_backup(db, user, router.id)
|
||||
result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
import io
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import paramiko
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -11,6 +15,30 @@ from app.services.swos_beta_service import swos_beta_service
|
||||
|
||||
|
||||
class RouterService:
|
||||
def ping(self, router: Router):
|
||||
if getattr(router, 'disable_ping', False):
|
||||
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': True}
|
||||
count_flag = '-n' if platform.system().lower().startswith('win') else '-c'
|
||||
timeout_flag = '-w' if platform.system().lower().startswith('win') else '-W'
|
||||
command = ['ping', count_flag, '1', timeout_flag, '1', router.host]
|
||||
try:
|
||||
completed = subprocess.run(command, capture_output=True, text=True, timeout=3, check=False)
|
||||
output = completed.stdout + "\n" + completed.stderr
|
||||
if completed.returncode != 0:
|
||||
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': False}
|
||||
match = re.search(r'time[=<]\s*([0-9]+(?:[.,][0-9]+)?)\s*ms', output, re.IGNORECASE)
|
||||
latency = float(match.group(1).replace(',', '.')) if match else None
|
||||
return {'router_id': router.id, 'reachable': True, 'latency_ms': latency, 'disabled': False}
|
||||
except Exception:
|
||||
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': False}
|
||||
|
||||
def ping_many(self, routers: list[Router]):
|
||||
if not routers:
|
||||
return []
|
||||
max_workers = min(8, max(1, len(routers)))
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
return list(executor.map(self.ping, routers))
|
||||
|
||||
def _load_pkey(self, ssh_key_str: str):
|
||||
key_str = (ssh_key_str or "").strip()
|
||||
key_buffer = io.StringIO(key_str)
|
||||
|
||||
@@ -212,6 +212,8 @@ class SchedulerService:
|
||||
with SessionLocal() as db:
|
||||
routers = db.query(Router).all()
|
||||
for router in routers:
|
||||
if router.disable_all_backups or router.disable_export_backups:
|
||||
continue
|
||||
try:
|
||||
backup_service.export_router(db, type('U', (), {'id': router.owner_id})(), router.id)
|
||||
except Exception as exc:
|
||||
@@ -221,6 +223,8 @@ class SchedulerService:
|
||||
with SessionLocal() as db:
|
||||
routers = db.query(Router).all()
|
||||
for router in routers:
|
||||
if router.disable_all_backups or router.disable_binary_backups:
|
||||
continue
|
||||
try:
|
||||
backup_service.binary_backup(db, type('U', (), {'id': router.owner_id})(), router.id)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user