"""Service prerequis — verification automatique des serveurs d'une campagne Check basique (TCP) + check approfondi (SSH) si accessible""" import socket import paramiko import os from sqlalchemy import text # Seuils espace disque (Mo) DISK_ROOT_MIN_MB = 1200 # 1.2 Go minimum sur / DISK_VAR_MIN_MB = 800 # 800 Mo minimum sur /var ou /var/log # SSH config (pour check approfondi) SSH_KEY = "/opt/patchcenter/keys/id_rsa_cybglobal.pem" # Copier la cle ici SSH_USER = "cybsecope" SSH_TIMEOUT = 10 DNS_SUFFIXES = ["", ".sanef.groupe", ".sanef-rec.fr", ".sanef.fr"] def check_prereqs_campaign(db, campaign_id): """Verifie les prereqs de tous les serveurs pending d'une campagne.""" sessions = db.execute(text(""" SELECT ps.id, s.hostname, s.os_family, s.etat, s.licence_support, s.machine_type, s.satellite_host, s.ssh_method, d.code as domain_code, z.name as zone FROM patch_sessions ps JOIN servers s ON ps.server_id = s.id LEFT JOIN domain_environments de ON s.domain_env_id = de.id LEFT JOIN domains d ON de.domain_id = d.id LEFT JOIN zones z ON s.zone_id = z.id WHERE ps.campaign_id = :cid AND ps.status = 'pending' """), {"cid": campaign_id}).fetchall() checked = 0 for s in sessions: result = _check_server(s) _save_result(db, s.id, result) checked += 1 auto_excluded = _auto_exclude(db, campaign_id) db.commit() return checked, auto_excluded def _resolve_host(hostname): """Trouve un FQDN resolvable et joignable sur port 22""" for suffix in DNS_SUFFIXES: target = hostname + suffix try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) r = sock.connect_ex((target, 22)) sock.close() if r == 0: return target except (socket.gaierror, socket.timeout, OSError): continue return None def _ssh_connect(target): """Tente une connexion SSH par cle. Retourne le client ou None.""" if not os.path.exists(SSH_KEY): return None for loader in [paramiko.RSAKey.from_private_key_file, paramiko.Ed25519Key.from_private_key_file]: try: key = loader(SSH_KEY) client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(target, port=22, username=SSH_USER, pkey=key, timeout=SSH_TIMEOUT, look_for_keys=False, allow_agent=False) return client except Exception: continue return None def _ssh_cmd(client, cmd, timeout=8): """Execute une commande SSH et retourne stdout""" try: stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout) return stdout.read().decode("utf-8", errors="replace").strip() except Exception: return "" def _check_disk(client): """Verifie l'espace disque via SSH. Retourne (root_mb, var_mb, ok)""" output = _ssh_cmd(client, "df -BM --output=target,avail 2>/dev/null | grep -E '^/ |^/var' | head -5") root_mb = None var_mb = None for line in output.split("\n"): parts = line.split() if len(parts) >= 2: mount = parts[0] try: avail = int(parts[1].replace("M", "")) except ValueError: continue if mount == "/": root_mb = avail elif mount in ("/var", "/var/log"): var_mb = avail if var_mb is None or avail < (var_mb or 9999) else var_mb # Si /var pas monte separement, il est dans / if var_mb is None and root_mb is not None: var_mb = root_mb ok = True if root_mb is not None and root_mb < DISK_ROOT_MIN_MB: ok = False if var_mb is not None and var_mb < DISK_VAR_MIN_MB: ok = False return root_mb, var_mb, ok def _check_satellite_ssh(client): """Verifie la connectivite Satellite via SSH""" output = _ssh_cmd(client, "subscription-manager identity 2>/dev/null | grep -i 'org' || echo 'not_registered'") if "not_registered" in output or not output: return "ko" return "ok" def _check_server(s): """Verifie un serveur. Check basique + approfondi si SSH OK.""" result = { "ssh": "pending", "satellite": "pending", "rollback": None, "disk_root_mb": None, "disk_var_mb": None, "disk_ok": None, "eligible": True, "exclude_reason": None, "exclude_detail": None, } # 1. Eligibilite de base if s.licence_support == 'eol': result["eligible"] = False result["exclude_reason"] = "eol" result["exclude_detail"] = "Licence EOL — serveur non supporte" return result if s.etat != 'en_production': result["eligible"] = False result["exclude_reason"] = "non_patchable" result["exclude_detail"] = f"Etat: {s.etat}" return result # 2. Connectivite TCP port 22 target = _resolve_host(s.hostname) if not target: result["ssh"] = "ko" result["eligible"] = False result["exclude_reason"] = "creneau_inadequat" result["exclude_detail"] = f"Port 22 injoignable ({s.hostname})" return result result["ssh"] = "ok" # 3. Rollback if s.machine_type == 'vm': result["rollback"] = "snapshot" else: result["rollback"] = "na" # 4. Check approfondi via SSH (si cle dispo) client = _ssh_connect(target) if client: try: # Espace disque root_mb, var_mb, disk_ok = _check_disk(client) result["disk_root_mb"] = root_mb result["disk_var_mb"] = var_mb result["disk_ok"] = disk_ok if not disk_ok: detail_parts = [] if root_mb is not None and root_mb < DISK_ROOT_MIN_MB: detail_parts.append(f"/ = {root_mb}Mo (min {DISK_ROOT_MIN_MB}Mo)") if var_mb is not None and var_mb < DISK_VAR_MIN_MB: detail_parts.append(f"/var = {var_mb}Mo (min {DISK_VAR_MIN_MB}Mo)") result["eligible"] = False result["exclude_reason"] = "creneau_inadequat" result["exclude_detail"] = "Espace disque insuffisant: " + ", ".join(detail_parts) # Satellite reel (Linux) if s.os_family == 'linux': result["satellite"] = _check_satellite_ssh(client) else: result["satellite"] = "na" finally: try: client.close() except Exception: pass else: # Pas de connexion SSH approfondie — fallback sur les donnees en base if s.os_family == 'linux': result["satellite"] = "ok" if s.satellite_host else "ko" else: result["satellite"] = "na" result["disk_ok"] = None # Non verifie return result def _save_result(db, session_id, result): """Sauvegarde les resultats prereqs""" all_ok = (result["eligible"] and result["ssh"] == "ok" and result.get("disk_ok") is not False) db.execute(text(""" UPDATE patch_sessions SET prereq_ssh = :ssh, prereq_satellite = :sat, rollback_method = COALESCE(rollback_method, :rb), prereq_disk_root = :dr, prereq_disk_log = :dv, prereq_disk_root_mb = :drm, prereq_disk_var_mb = :dvm, prereq_disk_ok = :dok, prereq_validated = :valid, prereq_date = now() WHERE id = :id """), { "id": session_id, "ssh": result["ssh"], "sat": result["satellite"], "rb": result["rollback"], "dr": result["disk_root_mb"], "dv": result["disk_var_mb"], "drm": result["disk_root_mb"], "dvm": result["disk_var_mb"], "dok": result["disk_ok"], "valid": all_ok, }) def _auto_exclude(db, campaign_id): """Exclut les serveurs non eligibles apres verification""" non_eligible = db.execute(text(""" SELECT ps.id, s.hostname, s.licence_support, s.etat, ps.prereq_ssh, ps.prereq_disk_ok FROM patch_sessions ps JOIN servers s ON ps.server_id = s.id WHERE ps.campaign_id = :cid AND ps.status = 'pending' AND (s.licence_support = 'eol' OR s.etat != 'en_production' OR ps.prereq_ssh = 'ko' OR ps.prereq_disk_ok = false) """), {"cid": campaign_id}).fetchall() count = 0 for s in non_eligible: if s.licence_support == 'eol': reason, detail = "eol", "Licence EOL — auto-exclu" elif s.etat != 'en_production': reason, detail = "non_patchable", f"Etat {s.etat} — auto-exclu" elif s.prereq_disk_ok is False: reason, detail = "creneau_inadequat", "Espace disque insuffisant — auto-exclu" else: reason, detail = "creneau_inadequat", "SSH injoignable — auto-exclu" db.execute(text(""" UPDATE patch_sessions SET status = 'excluded', exclusion_reason = :r, exclusion_detail = :d, excluded_by = 'system', excluded_at = now() WHERE id = :id """), {"id": s.id, "r": reason, "d": detail}) count += 1 if count > 0: total = db.execute(text( "SELECT COUNT(*) FROM patch_sessions WHERE campaign_id = :cid AND status NOT IN ('excluded','cancelled')" ), {"cid": campaign_id}).scalar() db.execute(text("UPDATE campaigns SET total_servers = :c WHERE id = :cid"), {"c": total, "cid": campaign_id}) return count def check_single_prereq(db, session_id): """Verifie les prereqs d'un seul serveur""" s = db.execute(text(""" SELECT ps.id, s.hostname, s.os_family, s.etat, s.licence_support, s.machine_type, s.satellite_host, s.ssh_method, d.code as domain_code, z.name as zone FROM patch_sessions ps JOIN servers s ON ps.server_id = s.id LEFT JOIN domain_environments de ON s.domain_env_id = de.id LEFT JOIN domains d ON de.domain_id = d.id LEFT JOIN zones z ON s.zone_id = z.id WHERE ps.id = :sid """), {"sid": session_id}).fetchone() if not s: return None result = _check_server(s) _save_result(db, session_id, result) db.commit() return result