175 lines
7.1 KiB
Python
175 lines
7.1 KiB
Python
import re
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
|
|
|
from app.schemas.swos_beta import SwosBetaCredentials, SwosBetaProbeResponse
|
|
|
|
|
|
@dataclass
|
|
class DownloadedSwosBackup:
|
|
filename: str
|
|
content: bytes
|
|
content_type: str
|
|
auth_mode: str
|
|
base_url: str
|
|
|
|
|
|
class SwosBetaService:
|
|
timeout_seconds = 12
|
|
|
|
def probe(self, payload: SwosBetaCredentials) -> SwosBetaProbeResponse:
|
|
base_url = self._build_base_url(payload.host, payload.port)
|
|
response, auth_mode = self._request_with_fallback('GET', base_url, payload)
|
|
html = response.text if 'text' in (response.headers.get('content-type') or '').lower() else ''
|
|
title = self._extract_title(html)
|
|
|
|
backup_response, _ = self._request_with_fallback('GET', f'{base_url}/backup.swb', payload, allow_text_fallback=False)
|
|
backup_ok = backup_response.status_code == 200 and len(backup_response.content) > 0
|
|
|
|
return SwosBetaProbeResponse(
|
|
success=response.ok,
|
|
base_url=base_url,
|
|
status_code=response.status_code,
|
|
auth_mode=auth_mode,
|
|
page_title=title,
|
|
content_type=response.headers.get('content-type'),
|
|
server=response.headers.get('server'),
|
|
save_backup_visible='save backup' in html.lower(),
|
|
backup_endpoint_ok=backup_ok,
|
|
note='SwitchOS jest obsługiwany bezpośrednio w liście urządzeń.'
|
|
)
|
|
|
|
def probe_router(self, router, global_settings) -> dict:
|
|
payload = self.credentials_from_router(router, global_settings)
|
|
tested_at = datetime.utcnow()
|
|
try:
|
|
result = self.probe(payload)
|
|
return {
|
|
'success': result.success,
|
|
'tested_at': tested_at,
|
|
'model': 'SwitchOS',
|
|
'uptime': f'HTTP {result.status_code}',
|
|
'hostname': result.page_title or router.name,
|
|
'version': None,
|
|
'error': None,
|
|
'transport': 'http',
|
|
'server': result.server,
|
|
'auth_mode': result.auth_mode,
|
|
'http_status': str(result.status_code),
|
|
'backup_available': result.backup_endpoint_ok,
|
|
}
|
|
except Exception as exc:
|
|
return {
|
|
'success': False,
|
|
'tested_at': tested_at,
|
|
'model': 'SwitchOS',
|
|
'uptime': 'HTTP',
|
|
'hostname': router.name,
|
|
'version': None,
|
|
'error': str(exc),
|
|
'transport': 'http',
|
|
'server': None,
|
|
'auth_mode': None,
|
|
'http_status': None,
|
|
'backup_available': None,
|
|
}
|
|
|
|
def credentials_from_router(self, router, global_settings) -> SwosBetaCredentials:
|
|
username = (getattr(router, 'ssh_user', None) or '').strip() or (getattr(global_settings, 'default_switchos_username', None) or '').strip()
|
|
password = (getattr(router, 'ssh_password', None) or '').strip() or (getattr(global_settings, 'default_switchos_password', None) or '').strip()
|
|
if not username:
|
|
raise ValueError('Brak użytkownika SwitchOS. Ustaw dane w urządzeniu albo w ustawieniach globalnych.')
|
|
return SwosBetaCredentials(
|
|
host=router.host,
|
|
port=router.port or 80,
|
|
username=username,
|
|
password=password,
|
|
label=router.name,
|
|
)
|
|
|
|
def download_backup(self, payload: SwosBetaCredentials) -> DownloadedSwosBackup:
|
|
base_url = self._build_base_url(payload.host, payload.port)
|
|
response, auth_mode = self._request_with_fallback('GET', f'{base_url}/backup.swb', payload, allow_text_fallback=False)
|
|
if response.status_code != 200:
|
|
raise ValueError(f'Urządzenie zwróciło kod HTTP {response.status_code} dla /backup.swb.')
|
|
if not response.content:
|
|
raise ValueError('Urządzenie zwróciło pusty plik backupu.')
|
|
|
|
filename = self._build_filename(payload)
|
|
content_type = response.headers.get('content-type') or 'application/octet-stream'
|
|
return DownloadedSwosBackup(
|
|
filename=filename,
|
|
content=response.content,
|
|
content_type=content_type,
|
|
auth_mode=auth_mode,
|
|
base_url=base_url,
|
|
)
|
|
|
|
def download_backup_for_router(self, router, global_settings) -> DownloadedSwosBackup:
|
|
return self.download_backup(self.credentials_from_router(router, global_settings))
|
|
|
|
def _request_with_fallback(self, method: str, url: str, payload: SwosBetaCredentials, allow_text_fallback: bool = True):
|
|
attempts = []
|
|
auth_variants = [
|
|
('digest', HTTPDigestAuth(payload.username, payload.password)),
|
|
('basic', HTTPBasicAuth(payload.username, payload.password)),
|
|
]
|
|
if allow_text_fallback:
|
|
auth_variants.append(('none', None))
|
|
|
|
last_response = None
|
|
for label, auth in auth_variants:
|
|
try:
|
|
response = requests.request(
|
|
method,
|
|
url,
|
|
auth=auth,
|
|
timeout=self.timeout_seconds,
|
|
allow_redirects=True,
|
|
)
|
|
last_response = response
|
|
if response.status_code < 400:
|
|
return response, label
|
|
attempts.append(f'{label}:{response.status_code}')
|
|
except requests.RequestException as exc:
|
|
attempts.append(f'{label}:{exc.__class__.__name__}')
|
|
|
|
if last_response is not None:
|
|
raise ValueError(f'Nie udało się połączyć ze SwitchOS ({", ".join(attempts)}).')
|
|
raise ValueError('Nie udało się połączyć ze SwitchOS.')
|
|
|
|
def _build_base_url(self, host: str, port: int) -> str:
|
|
raw = host.strip()
|
|
parsed = urlparse(raw if '://' in raw else f'http://{raw}')
|
|
scheme = parsed.scheme or 'http'
|
|
if scheme not in {'http', 'https'}:
|
|
raise ValueError('Dozwolone są tylko adresy HTTP lub HTTPS.')
|
|
if not parsed.hostname:
|
|
raise ValueError('Nieprawidłowy adres hosta.')
|
|
resolved_port = parsed.port or port
|
|
base = f'{scheme}://{parsed.hostname}'
|
|
if resolved_port not in {80, 443} or (scheme == 'http' and resolved_port != 80) or (scheme == 'https' and resolved_port != 443):
|
|
base = f'{base}:{resolved_port}'
|
|
return base.rstrip('/')
|
|
|
|
def _extract_title(self, html: str) -> str | None:
|
|
if not html:
|
|
return None
|
|
match = re.search(r'<title>(.*?)</title>', html, flags=re.IGNORECASE | re.DOTALL)
|
|
if not match:
|
|
return None
|
|
return re.sub(r'\s+', ' ', match.group(1)).strip() or None
|
|
|
|
def _build_filename(self, payload: SwosBetaCredentials) -> str:
|
|
label = payload.label or payload.host
|
|
safe = re.sub(r'[^A-Za-z0-9._-]+', '-', label).strip('-') or 'switchos'
|
|
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
|
|
return f'{safe}-switchos-{timestamp}.swb'
|
|
|
|
|
|
swos_beta_service = SwosBetaService()
|