205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
"""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)
|
|
# .strip() défensif : un copier-coller peut introduire \n / \r / espaces
|
|
vc_user = (_get_secret(db, "vcenter_user") or _get_secret(db, "vsphere_user") or "").strip()
|
|
vc_pass = (_get_secret(db, "vcenter_pass") or _get_secret(db, "vsphere_pass") or "").strip()
|
|
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}
|
|
|
|
login_failures = []
|
|
not_found_on = []
|
|
for vc in vcenters:
|
|
si = _connect_vcenter(vc.endpoint, vc_user, vc_pass)
|
|
if not si:
|
|
login_failures.append(vc.name)
|
|
continue
|
|
try:
|
|
vm = _find_vm(si, search_name)
|
|
if vm:
|
|
ok, msg = _take_snapshot(vm, snap_name,
|
|
description=f"PatchCenter snapshot {hostname}")
|
|
return {"ok": ok, "vcenter": vc.name, "detail": msg}
|
|
not_found_on.append(vc.name)
|
|
finally:
|
|
try:
|
|
Disconnect(si)
|
|
except Exception:
|
|
pass
|
|
|
|
# Tous les vCenters épuisés sans match → message précis
|
|
if login_failures and not not_found_on:
|
|
return {
|
|
"ok": False, "vcenter": "",
|
|
"detail": (f"Échec login sur tous les vCenters ({', '.join(login_failures)}). "
|
|
"Vérifier vsphere_user/vsphere_pass dans Settings > vSphere "
|
|
"(format attendu : user@vsphere.local ou DOMAIN\\\\user)."),
|
|
"skipped": True,
|
|
}
|
|
if login_failures:
|
|
return {
|
|
"ok": False, "vcenter": "",
|
|
"detail": (f"VM '{search_name}' non trouvée sur {', '.join(not_found_on)} ; "
|
|
f"login KO sur {', '.join(login_failures)}"),
|
|
}
|
|
return {"ok": False, "vcenter": "",
|
|
"detail": f"VM '{search_name}' non trouvée sur : {', '.join(not_found_on)}"}
|