"""Service snapshot QuickWin — prise de snapshots VM via vSphere/pyvmomi Ordre de recherche des VM sur les vCenters: - Hors-prod: Senlis (vpgesavcs1) → Nanterre (vpmetavcs1) → DR (vpsicavcs1) - Prod: Nanterre (vpmetavcs1) → Senlis (vpgesavcs1) → DR (vpsicavcs1) Physiques: pas de snapshot, alerte Commvault.""" import ssl import logging from datetime import datetime log = logging.getLogger("quickwin.snapshot") try: from pyVim.connect import SmartConnect, Disconnect from pyVmomi import vim PYVMOMI_OK = True except ImportError: PYVMOMI_OK = False log.warning("pyvmomi non disponible — snapshots impossibles") def _get_secret(db, key): try: from ..services.secrets_service import get_secret return get_secret(db, key) except Exception: return None def _connect_vcenter(endpoint, user, password): """Connexion a un vCenter. Retourne un ServiceInstance ou None.""" try: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE si = SmartConnect(host=endpoint, user=user, pwd=password, sslContext=ctx) return si except Exception as e: log.warning(f"Connexion vCenter {endpoint} echouee: {e}") return None def _find_vm(si, vm_name): """Cherche une VM par nom dans le vCenter. Matching tolérant : 1. nom exact (insensible à la casse) 2. partie courte avant le 1er '.' (ex: 'vrexpbtex1' == 'vrexpbtex1.sanef.groupe') Logue un échantillon de VMs vues pour faciliter le debug en cas d'échec. """ target_full = (vm_name or "").lower().strip() short = target_full.split(".")[0] if not short: return None content = si.RetrieveContent() container = content.viewManager.CreateContainerView( content.rootFolder, [vim.VirtualMachine], True) total = 0 exact_match = None short_match = None samples = [] try: for vm in container.view: total += 1 if not vm.name: continue n = vm.name.lower().strip() if exact_match is None and n == target_full: exact_match = vm elif short_match is None and n.split(".")[0] == short: short_match = vm if len(samples) < 5 and n.startswith(short[:3]): samples.append(vm.name) finally: container.Destroy() found = exact_match or short_match if found is None: log.info(f"_find_vm({vm_name}): no match parmi {total} VMs ; " f"samples startswith '{short[:3]}': {samples}") else: kind = "exact" if exact_match else "short" log.info(f"_find_vm({vm_name}): {kind} match → {found.name}") return found def _take_snapshot(vm, snap_name, description=""): """Prend un snapshot de la VM. Retourne (ok, message).""" try: task = vm.CreateSnapshot_Task( name=snap_name, description=description, memory=False, quiesce=True, ) # Attendre la fin du task while task.info.state in (vim.TaskInfo.State.queued, vim.TaskInfo.State.running): import time time.sleep(2) if task.info.state == vim.TaskInfo.State.success: return True, "Snapshot OK" else: err = str(task.info.error) if task.info.error else "Echec inconnu" return False, f"Snapshot echoue: {err}" except Exception as e: return False, f"Erreur snapshot: {e}" def get_vcenter_order(db, branch): """Retourne la liste ordonnee des vCenters selon la branche. Endpoints reconnus : - gestion : vpgesavcs1(.sanef.groupe) (= Senlis) - metier : vpmetavcs1(.sanef.groupe) (= Nanterre) - DR/SIA : vpsiaavcs1(.sanef.groupe) (fallback ultime) Matching tolérant par endpoint OU par name de la table vcenters. Ordre : prod : metier → gestion → dr hprod : gestion → metier → dr""" from sqlalchemy import text vcenters = db.execute(text( "SELECT id, name, endpoint FROM vcenters WHERE is_active = true ORDER BY id" )).fetchall() vc_map = {} for vc in vcenters: ep = (vc.endpoint or "").lower() nm = (vc.name or "").lower() if "vpgesavcs1" in ep or "gestion" in nm: vc_map["gestion"] = vc elif "vpmetavcs1" in ep or "metier" in nm: vc_map["metier"] = vc elif "vpsiaavcs1" in ep or "vpsicavcs1" in ep or "sia" in nm or nm == "dr": vc_map["dr"] = vc else: vc_map.setdefault("other", []).append(vc) if branch == "prod": order = [vc_map.get("metier"), vc_map.get("gestion"), vc_map.get("dr")] else: order = [vc_map.get("gestion"), vc_map.get("metier"), vc_map.get("dr")] return [v for v in order if v is not None] def snapshot_server(hostname, vm_name, branch, db, snap_name=None): """Prend un snapshot pour un serveur. Cherche la VM sur les vCenters dans l'ordre selon la branche. Retourne dict: {ok, vcenter, detail, skipped}""" if not PYVMOMI_OK: return {"ok": False, "vcenter": "", "detail": "pyvmomi non installe", "skipped": True} # Fallback historique : vcenter_user/vcenter_pass (legacy) ou vsphere_user/vsphere_pass (Settings UI) vc_user = _get_secret(db, "vcenter_user") or _get_secret(db, "vsphere_user") vc_pass = _get_secret(db, "vcenter_pass") or _get_secret(db, "vsphere_pass") if not vc_user or not vc_pass: return {"ok": False, "vcenter": "", "detail": "Credentials vCenter manquants (renseigner vsphere_user/vsphere_pass dans Settings > vSphere)", "skipped": True} search_name = vm_name or hostname if not snap_name: snap_name = f"QW_{datetime.now().strftime('%Y%m%d_%H%M')}" vcenters = get_vcenter_order(db, branch) if not vcenters: return {"ok": False, "vcenter": "", "detail": "Aucun vCenter actif configure", "skipped": True} for vc in vcenters: si = _connect_vcenter(vc.endpoint, vc_user, vc_pass) if not si: continue try: vm = _find_vm(si, search_name) if vm: ok, msg = _take_snapshot(vm, snap_name, description=f"QuickWin auto-snapshot {hostname}") return {"ok": ok, "vcenter": vc.name, "detail": msg} finally: try: Disconnect(si) except Exception: pass tried = ", ".join(vc.name for vc in vcenters) return {"ok": False, "vcenter": "", "detail": f"VM '{search_name}' non trouvee sur: {tried}"}