314 lines
11 KiB
Python
314 lines
11 KiB
Python
"""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
|
|
|
|
# Mode demo : tout passe OK sans verifier (hors reseau SANEF)
|
|
DEMO_MODE = True # Passer a False en production SANEF
|
|
|
|
# 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,
|
|
}
|
|
|
|
# Mode demo : tout OK
|
|
if DEMO_MODE:
|
|
result["ssh"] = "ok"
|
|
result["satellite"] = "ok" if s.os_family == 'linux' else "na"
|
|
result["rollback"] = "snapshot" if s.machine_type == 'vm' else "na"
|
|
result["disk_root_mb"] = 5000
|
|
result["disk_var_mb"] = 3000
|
|
result["disk_ok"] = True
|
|
# Quand meme exclure les EOL et decom
|
|
if s.licence_support == 'obsolete':
|
|
result["eligible"] = False
|
|
result["exclude_reason"] = "obsolete"
|
|
result["exclude_detail"] = "Licence EOL"
|
|
elif s.etat != 'production':
|
|
result["eligible"] = False
|
|
result["exclude_reason"] = "non_patchable"
|
|
result["exclude_detail"] = f"Etat: {s.etat}"
|
|
return result
|
|
|
|
# 1. Eligibilite de base
|
|
if s.licence_support == 'obsolete':
|
|
result["eligible"] = False
|
|
result["exclude_reason"] = "obsolete"
|
|
result["exclude_detail"] = "Licence EOL — serveur non supporte"
|
|
return result
|
|
|
|
if s.etat != '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 = 'obsolete'
|
|
OR s.etat != '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 == 'obsolete':
|
|
reason, detail = "obsolete", "Licence EOL — auto-exclu"
|
|
elif s.etat != '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
|