221 lines
9.1 KiB
Python
221 lines
9.1 KiB
Python
from datetime import datetime
|
|
import io
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.router import Router
|
|
from app.services.log_service import log_service
|
|
from app.services.swos_beta_service import swos_beta_service
|
|
|
|
|
|
class RouterService:
|
|
def _load_pkey(self, ssh_key_str: str):
|
|
key_str = (ssh_key_str or "").strip()
|
|
key_buffer = io.StringIO(key_str)
|
|
loaders = [
|
|
paramiko.RSAKey.from_private_key,
|
|
paramiko.Ed25519Key.from_private_key,
|
|
paramiko.ECDSAKey.from_private_key,
|
|
]
|
|
last_error = None
|
|
for loader in loaders:
|
|
key_buffer.seek(0)
|
|
try:
|
|
return loader(key_buffer)
|
|
except Exception as exc:
|
|
last_error = exc
|
|
raise ValueError("Failed to load SSH private key") from last_error
|
|
|
|
def _connect(self, router: Router, global_ssh_key: str | None = None):
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
key_source = router.ssh_key.strip() if router.ssh_key and router.ssh_key.strip() else (global_ssh_key or "")
|
|
if key_source:
|
|
pkey = self._load_pkey(key_source)
|
|
client.connect(router.host, port=router.port, username=router.ssh_user, pkey=pkey, timeout=10)
|
|
else:
|
|
client.connect(
|
|
router.host,
|
|
port=router.port,
|
|
username=router.ssh_user,
|
|
password=router.ssh_password,
|
|
timeout=10,
|
|
allow_agent=False,
|
|
look_for_keys=False,
|
|
banner_timeout=10,
|
|
)
|
|
return client
|
|
|
|
def export(self, router: Router, global_ssh_key: str | None = None) -> str:
|
|
if router.device_type != 'routeros':
|
|
raise ValueError('Export tekstowy jest dostępny tylko dla RouterOS.')
|
|
client = self._connect(router, global_ssh_key)
|
|
_, stdout, _ = client.exec_command('/export')
|
|
output = stdout.read().decode('utf-8', errors='ignore')
|
|
client.close()
|
|
return output
|
|
|
|
def binary_backup(self, router: Router, backup_name: str, local_path: str, global_ssh_key: str | None = None, global_settings=None) -> str:
|
|
if router.device_type == 'switchos':
|
|
downloaded = swos_beta_service.download_backup_for_router(router, global_settings)
|
|
Path(local_path).write_bytes(downloaded.content)
|
|
return local_path
|
|
|
|
client = self._connect(router, global_ssh_key)
|
|
_, stdout, _ = client.exec_command(f'/system backup save name={backup_name}')
|
|
stdout.channel.recv_exit_status()
|
|
sftp = client.open_sftp()
|
|
remote_file = f'{backup_name}.backup'
|
|
sftp.get(remote_file, local_path)
|
|
try:
|
|
sftp.remove(remote_file)
|
|
except Exception:
|
|
pass
|
|
sftp.close()
|
|
client.close()
|
|
return local_path
|
|
|
|
def upload_backup(self, router: Router, local_backup_path: str, global_ssh_key: str | None = None):
|
|
if router.device_type != 'routeros':
|
|
raise ValueError('Przywracanie plików jest dostępne tylko dla RouterOS.')
|
|
client = self._connect(router, global_ssh_key)
|
|
sftp = client.open_sftp()
|
|
target_name = Path(local_backup_path).name
|
|
sftp.put(local_backup_path, target_name)
|
|
sftp.close()
|
|
client.close()
|
|
|
|
def _probe_routeros_connection(self, router: Router, global_ssh_key: str | None = None):
|
|
tested_at = datetime.utcnow()
|
|
try:
|
|
client = self._connect(router, global_ssh_key)
|
|
_, stdout, _ = client.exec_command('/system resource print without-paging')
|
|
resource_output = stdout.read().decode('utf-8', errors='ignore')
|
|
_, stdout, _ = client.exec_command('/system identity print')
|
|
identity_output = stdout.read().decode('utf-8', errors='ignore')
|
|
client.close()
|
|
model = 'Unknown'
|
|
uptime = 'Unknown'
|
|
hostname = 'Unknown'
|
|
version = 'Unknown'
|
|
for line in resource_output.splitlines():
|
|
if 'board-name' in line:
|
|
model = line.split(':', 1)[1].strip()
|
|
if 'uptime' in line:
|
|
uptime = line.split(':', 1)[1].strip()
|
|
if 'version' in line:
|
|
version = line.split(':', 1)[1].strip()
|
|
for line in identity_output.splitlines():
|
|
if 'name' in line:
|
|
hostname = line.split(':', 1)[1].strip()
|
|
return {
|
|
'success': True,
|
|
'tested_at': tested_at,
|
|
'model': model,
|
|
'uptime': uptime,
|
|
'hostname': hostname,
|
|
'version': version,
|
|
'error': None,
|
|
'transport': 'ssh',
|
|
'server': None,
|
|
'auth_mode': 'ssh',
|
|
'http_status': None,
|
|
'backup_available': None,
|
|
}
|
|
except Exception as exc:
|
|
return {
|
|
'success': False,
|
|
'tested_at': tested_at,
|
|
'model': 'Unknown',
|
|
'uptime': 'Unknown',
|
|
'hostname': router.name,
|
|
'version': None,
|
|
'error': str(exc),
|
|
'transport': 'ssh',
|
|
'server': None,
|
|
'auth_mode': 'ssh',
|
|
'http_status': None,
|
|
'backup_available': None,
|
|
}
|
|
|
|
def probe_connection(self, router: Router, global_ssh_key: str | None = None, global_settings=None):
|
|
if router.device_type == 'switchos':
|
|
return swos_beta_service.probe_router(router, global_settings)
|
|
return self._probe_routeros_connection(router, global_ssh_key)
|
|
|
|
def _store_connection_result(self, db: Session, router: Router, result: dict):
|
|
router.last_connection_status = result['success']
|
|
router.last_connection_tested_at = result['tested_at']
|
|
router.last_connection_error = result.get('error')
|
|
router.last_connection_hostname = result.get('hostname')
|
|
router.last_connection_model = result.get('model')
|
|
router.last_connection_version = result.get('version')
|
|
router.last_connection_uptime = result.get('uptime')
|
|
router.last_connection_transport = result.get('transport')
|
|
router.last_connection_server = result.get('server')
|
|
router.last_connection_auth_mode = result.get('auth_mode')
|
|
router.last_connection_http_status = result.get('http_status')
|
|
router.last_connection_backup_available = result.get('backup_available')
|
|
db.add(router)
|
|
db.commit()
|
|
db.refresh(router)
|
|
return result
|
|
|
|
def _device_label(self, router: Router) -> str:
|
|
platform = 'SwitchOS' if router.device_type == 'switchos' else 'RouterOS'
|
|
return f'{platform} device {router.name}'
|
|
|
|
def _build_connection_log_message(self, router: Router, result: dict) -> str:
|
|
device_label = self._device_label(router)
|
|
transport = result.get('transport') or 'unknown transport'
|
|
auth_mode = result.get('auth_mode')
|
|
http_status = result.get('http_status')
|
|
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}', 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:
|
|
details.append(f'auth={auth_mode}')
|
|
if http_status:
|
|
details.append(f'http={http_status}')
|
|
if server:
|
|
details.append(f'server={server}')
|
|
if backup_available is not None:
|
|
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 ''
|
|
if result.get('success'):
|
|
return f'Connection test OK for {device_label}{detail_suffix}'
|
|
|
|
error = result.get('error') or 'Unknown error'
|
|
return f'Connection test FAILED for {device_label}{detail_suffix}: {error}'
|
|
|
|
def test_connection(self, db: Session, router: Router, global_settings):
|
|
result = self.probe_connection(router, global_settings.global_ssh_key, global_settings)
|
|
stored_result = self._store_connection_result(db, router, result)
|
|
log_service.add(db, self._build_connection_log_message(router, stored_result))
|
|
return stored_result
|
|
|
|
|
|
router_service = RouterService()
|