extended logs

This commit is contained in:
Mateusz Gruszczyński
2026-04-13 12:28:06 +02:00
parent d661079bd1
commit 62fa4c7a5b
10 changed files with 178 additions and 8 deletions

View File

@@ -1,3 +1,4 @@
from datetime import date
import io import io
import zipfile import zipfile
@@ -18,6 +19,7 @@ def list_backups(
search: str | None = Query(default=None), search: str | None = Query(default=None),
backup_type: str | None = Query(default=None, pattern="^(export|binary)$"), backup_type: str | None = Query(default=None, pattern="^(export|binary)$"),
router_id: int | None = Query(default=None), router_id: int | None = Query(default=None),
created_on: date | None = Query(default=None),
sort_by: str = Query(default="created_at"), sort_by: str = Query(default="created_at"),
order: str = Query(default="desc", pattern="^(asc|desc)$"), order: str = Query(default="desc", pattern="^(asc|desc)$"),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
@@ -29,6 +31,7 @@ def list_backups(
search=search, search=search,
backup_type=backup_type, backup_type=backup_type,
router_id=router_id, router_id=router_id,
created_on=created_on,
sort_by=sort_by, sort_by=sort_by,
order=order, order=order,
) )

View File

@@ -1,5 +1,5 @@
import difflib import difflib
from datetime import datetime, timedelta, timezone from datetime import date, datetime, time, timedelta, timezone
from pathlib import Path from pathlib import Path
from fastapi import HTTPException from fastapi import HTTPException
@@ -146,6 +146,7 @@ class BackupService:
search: str | None = None, search: str | None = None,
backup_type: str | None = None, backup_type: str | None = None,
router_id: int | None = None, router_id: int | None = None,
created_on: date | None = None,
sort_by: str = 'created_at', sort_by: str = 'created_at',
order: str = 'desc', order: str = 'desc',
): ):
@@ -160,6 +161,10 @@ class BackupService:
query = query.filter(Backup.backup_type == backup_type) query = query.filter(Backup.backup_type == backup_type)
if router_id: if router_id:
query = query.filter(Backup.router_id == router_id) query = query.filter(Backup.router_id == router_id)
if created_on:
day_start = datetime.combine(created_on, time.min)
next_day = day_start + timedelta(days=1)
query = query.filter(Backup.created_at >= day_start, Backup.created_at < next_day)
sort_map = { sort_map = {
'created_at': Backup.created_at, 'created_at': Backup.created_at,

View File

@@ -173,14 +173,35 @@ class RouterService:
auth_mode = result.get('auth_mode') auth_mode = result.get('auth_mode')
http_status = result.get('http_status') http_status = result.get('http_status')
backup_available = result.get('backup_available') backup_available = result.get('backup_available')
hostname = result.get('hostname')
model = result.get('model')
version = result.get('version')
uptime = result.get('uptime')
server = result.get('server')
details = [f'via {transport}'] details = [f'via {transport}', f'target={router.host}:{router.port}']
if router.device_type == 'routeros':
if router.ssh_user:
details.append(f'user={router.ssh_user}')
if hostname:
details.append(f'hostname={hostname}')
if model and model != 'Unknown':
details.append(f'model={model}')
if version and version != 'Unknown':
details.append(f'version={version}')
if uptime and uptime != 'Unknown':
details.append(f'uptime={uptime}')
else:
if auth_mode: if auth_mode:
details.append(f'auth={auth_mode}') details.append(f'auth={auth_mode}')
if http_status: if http_status:
details.append(f'http={http_status}') details.append(f'http={http_status}')
if server:
details.append(f'server={server}')
if backup_available is not None: if backup_available is not None:
details.append(f'backup_available={"yes" if backup_available else "no"}') details.append(f'backup_available={"yes" if backup_available else "no"}')
if hostname:
details.append(f'hostname={hostname}')
detail_suffix = f' ({", ".join(details)})' if details else '' detail_suffix = f' ({", ".join(details)})' if details else ''
if result.get('success'): if result.get('success'):

View File

@@ -0,0 +1,125 @@
from datetime import datetime
from app.db.session import SessionLocal
from app.models.backup import Backup
from fastapi.testclient import TestClient
from app.main import app
def _login(client: TestClient) -> dict[str, str]:
response = client.post('/api/auth/login', data={'username': 'admin', 'password': 'admin'})
token = response.json()['access_token']
return {'Authorization': f'Bearer {token}'}
def test_routeros_connection_test_creates_verbose_operation_log(monkeypatch):
from app.services import router_service as router_service_module
monkeypatch.setattr(
router_service_module.router_service,
'probe_connection',
lambda router, global_ssh_key=None, global_settings=None: {
'success': True,
'tested_at': datetime(2026, 4, 13, 10, 30, 0),
'model': 'RB5009UG+S+',
'uptime': '1d2h',
'hostname': 'rb5009-core',
'version': '7.18.2',
'error': None,
'transport': 'ssh',
'server': None,
'auth_mode': 'ssh',
'http_status': None,
'backup_available': None,
},
)
with TestClient(app) as client:
headers = _login(client)
create_response = client.post(
'/api/routers',
json={
'name': 'core01',
'device_type': 'routeros',
'host': '10.10.10.1',
'port': 2222,
'ssh_user': 'backup',
'ssh_password': 'secret',
'ssh_key': None,
},
headers=headers,
)
assert create_response.status_code == 200
device_id = create_response.json()['id']
response = client.get(f'/api/routers/{device_id}/test-connection', headers=headers)
assert response.status_code == 200
logs_response = client.get('/api/logs', headers=headers)
assert logs_response.status_code == 200
assert any(
'Connection test OK for RouterOS device core01' in item['message']
and 'via ssh' in item['message']
and 'target=10.10.10.1:2222' in item['message']
and 'user=backup' in item['message']
and 'hostname=rb5009-core' in item['message']
and 'model=RB5009UG+S+' in item['message']
and 'version=7.18.2' in item['message']
and 'uptime=1d2h' in item['message']
for item in logs_response.json()
)
def test_files_endpoint_filters_backups_by_created_on_date(monkeypatch):
from app.services import router_service as router_service_module
monkeypatch.setattr(
router_service_module.router_service,
'export',
lambda router, global_ssh_key=None: f'/system identity set name={router.name}',
)
with TestClient(app) as client:
headers = _login(client)
create_response = client.post(
'/api/routers',
json={
'name': 'archive01',
'device_type': 'routeros',
'host': '10.10.10.2',
'port': 22,
'ssh_user': 'admin',
'ssh_password': 'secret',
'ssh_key': None,
},
headers=headers,
)
assert create_response.status_code == 200
device_id = create_response.json()['id']
first = client.post(f'/api/backups/router/{device_id}/export', headers=headers)
second = client.post(f'/api/backups/router/{device_id}/export', headers=headers)
assert first.status_code == 200
assert second.status_code == 200
with SessionLocal() as db:
first_backup = db.query(Backup).filter(Backup.id == first.json()['id']).first()
second_backup = db.query(Backup).filter(Backup.id == second.json()['id']).first()
first_backup.created_at = datetime(2026, 4, 12, 9, 15, 0)
second_backup.created_at = datetime(2026, 4, 13, 11, 45, 0)
db.add(first_backup)
db.add(second_backup)
db.commit()
filtered = client.get(f'/api/backups?router_id={device_id}&created_on=2026-04-13', headers=headers)
assert filtered.status_code == 200
payload = filtered.json()
assert len(payload) == 1
assert payload[0]['created_at'].startswith('2026-04-13T11:45:00')
previous_day = client.get(f'/api/backups?router_id={device_id}&created_on=2026-04-12', headers=headers)
assert previous_day.status_code == 200
assert len(previous_day.json()) == 1
assert previous_day.json()[0]['created_at'].startswith('2026-04-12T09:15:00')

View File

@@ -32,6 +32,11 @@
<p-dropdown [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value"></p-dropdown> <p-dropdown [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value"></p-dropdown>
</span> </span>
<span class="form-field">
<label>{{ 'files.dateLabel' | translate }}</label>
<input pInputText type="date" [(ngModel)]="createdOn" [placeholder]="'files.datePlaceholder' | translate" />
</span>
<span class="form-field"> <span class="form-field">
<label>{{ 'files.sortLabel' | translate }}</label> <label>{{ 'files.sortLabel' | translate }}</label>
<p-dropdown [options]="sortOptions" [(ngModel)]="sortBy" optionLabel="label" optionValue="value"></p-dropdown> <p-dropdown [options]="sortOptions" [(ngModel)]="sortBy" optionLabel="label" optionValue="value"></p-dropdown>

View File

@@ -70,6 +70,7 @@ export class FilesPageComponent implements OnInit {
search = ''; search = '';
backupType: 'export' | 'binary' | '' = ''; backupType: 'export' | 'binary' | '' = '';
routerId: number | null = null; routerId: number | null = null;
createdOn = '';
sortBy = 'created_at'; sortBy = 'created_at';
order: 'asc' | 'desc' = 'desc'; order: 'asc' | 'desc' = 'desc';
diffText = ''; diffText = '';
@@ -187,6 +188,7 @@ export class FilesPageComponent implements OnInit {
if (this.search.trim()) params = params.set('search', this.search.trim()); if (this.search.trim()) params = params.set('search', this.search.trim());
if (this.backupType) params = params.set('backup_type', this.backupType); if (this.backupType) params = params.set('backup_type', this.backupType);
if (this.routerId !== null) params = params.set('router_id', String(this.routerId)); if (this.routerId !== null) params = params.set('router_id', String(this.routerId));
if (this.createdOn) params = params.set('created_on', this.createdOn);
this.api.http.get<BackupFile[]>(`${this.api.baseUrl}/backups`, { params }).subscribe({ this.api.http.get<BackupFile[]>(`${this.api.baseUrl}/backups`, { params }).subscribe({
next: (files) => { next: (files) => {
@@ -209,6 +211,7 @@ export class FilesPageComponent implements OnInit {
this.search = ''; this.search = '';
this.backupType = ''; this.backupType = '';
this.routerId = null; this.routerId = null;
this.createdOn = '';
this.sortBy = 'created_at'; this.sortBy = 'created_at';
this.order = 'desc'; this.order = 'desc';
this.load(); this.load();

View File

@@ -255,6 +255,8 @@
"searchPlaceholder": "Search by file or router", "searchPlaceholder": "Search by file or router",
"typeLabel": "Type", "typeLabel": "Type",
"routerLabel": "Device", "routerLabel": "Device",
"dateLabel": "Date",
"datePlaceholder": "Pick a date",
"sortLabel": "Sort by", "sortLabel": "Sort by",
"orderLabel": "Order", "orderLabel": "Order",
"allTypes": "All types", "allTypes": "All types",

View File

@@ -255,6 +255,8 @@
"searchPlaceholder": "Buscar por archivo o router", "searchPlaceholder": "Buscar por archivo o router",
"typeLabel": "Tipo", "typeLabel": "Tipo",
"routerLabel": "Dispositivo", "routerLabel": "Dispositivo",
"dateLabel": "Fecha",
"datePlaceholder": "Selecciona una fecha",
"sortLabel": "Ordenar por", "sortLabel": "Ordenar por",
"orderLabel": "Orden", "orderLabel": "Orden",
"allTypes": "Todos los tipos", "allTypes": "Todos los tipos",

View File

@@ -255,6 +255,8 @@
"searchPlaceholder": "Søk etter fil eller ruter", "searchPlaceholder": "Søk etter fil eller ruter",
"typeLabel": "Type", "typeLabel": "Type",
"routerLabel": "Enhet", "routerLabel": "Enhet",
"dateLabel": "Dato",
"datePlaceholder": "Velg dato",
"sortLabel": "Sorter etter", "sortLabel": "Sorter etter",
"orderLabel": "Rekkefølge", "orderLabel": "Rekkefølge",
"allTypes": "Alle typer", "allTypes": "Alle typer",

View File

@@ -255,6 +255,8 @@
"searchPlaceholder": "Szukaj po pliku lub routerze", "searchPlaceholder": "Szukaj po pliku lub routerze",
"typeLabel": "Typ", "typeLabel": "Typ",
"routerLabel": "Urządzenie", "routerLabel": "Urządzenie",
"dateLabel": "Data",
"datePlaceholder": "Wybierz datę",
"sortLabel": "Sortowanie", "sortLabel": "Sortowanie",
"orderLabel": "Kolejność", "orderLabel": "Kolejność",
"allTypes": "Wszystkie typy", "allTypes": "Wszystkie typy",