"""Probe remote host and register in SAC before agent deploy (WinRM / SSH).""" from __future__ import annotations import re from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.orm import Session from app.models import Host from app.services.agent_update_settings import PRODUCT_RDP, PRODUCT_SSH from app.services.linux_admin_settings import get_effective_linux_admin_config from app.services.ssh_connect import _is_ipv4, _ssh_output_hostname, test_ssh_connection from app.services.win_admin_settings import get_effective_win_admin_config from app.services.winrm_connect import test_winrm_connection _IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$") class ManualHostAddError(Exception): pass def validate_windows_target(target: str) -> str: text = (target or "").strip() if not text: raise ManualHostAddError("Укажите имя Windows-ПК") if _IPV4_RE.match(text): raise ManualHostAddError( "Для Windows укажите имя ПК (NetBIOS/DNS), не IP — WinRM с Kerberos по IP ненадёжен" ) short = text.split(".")[0].split("\\")[-1].strip() if not short or " " in short: raise ManualHostAddError("Некорректное имя хоста") return short def validate_linux_target(target: str) -> str: text = (target or "").strip() if not text: raise ManualHostAddError("Укажите IP или hostname Linux-хоста") return text def probe_windows_host(db: Session, hostname: str) -> str: cfg = get_effective_win_admin_config(db) if not cfg.configured: raise ManualHostAddError("Windows domain admin не настроен в SAC") probe = test_winrm_connection( target=hostname, user=cfg.user, password=cfg.password, ) if not probe.ok: raise ManualHostAddError(probe.message) return (probe.hostname or hostname).strip() def probe_linux_host(db: Session, target: str) -> tuple[str, str | None]: cfg = get_effective_linux_admin_config(db) if not cfg.configured: raise ManualHostAddError("Linux SSH admin не настроен в SAC") probe = test_ssh_connection( target=target, user=cfg.user, password=cfg.password, ) if not probe.ok: raise ManualHostAddError(probe.message) hostname = _ssh_output_hostname(probe.stdout) or target ipv4 = target if _is_ipv4(target) else None return hostname, ipv4 def get_or_create_manual_host( db: Session, *, hostname: str, product: str, os_family: str, ipv4: str | None = None, ) -> Host: host = db.scalar( select(Host).where(Host.hostname == hostname, Host.product == product).limit(1) ) now = datetime.now(timezone.utc) if host is None: host = Host( hostname=hostname, os_family=os_family, product=product, ipv4=ipv4, last_seen_at=now, ) db.add(host) else: host.os_family = os_family if ipv4: host.ipv4 = ipv4 host.last_seen_at = now db.flush() return host def prepare_manual_host_add(db: Session, *, platform: str, target: str) -> Host: platform_norm = (platform or "").strip().lower() if platform_norm == "windows": win_target = validate_windows_target(target) hostname = probe_windows_host(db, win_target) return get_or_create_manual_host( db, hostname=hostname, product=PRODUCT_RDP, os_family="windows", ) if platform_norm == "linux": linux_target = validate_linux_target(target) hostname, ipv4 = probe_linux_host(db, linux_target) return get_or_create_manual_host( db, hostname=hostname, product=PRODUCT_SSH, os_family="linux", ipv4=ipv4, ) raise ManualHostAddError("platform должен быть windows или linux")