Aligne la colonne servers.etat sur les valeurs iTop exactes au lieu des codes lowercase internes. Impact: - servers.etat stocke: Production, Implémentation, Stock, Obsolète, EOL, prêt, tests, Nouveau, A récupérer, Cassé, Cédé, En panne, Perdu, Recyclé, Occasion, A détruire, Volé - Remplace tous les 'production'/'obsolete'/'stock'/'eol'/'implementation' en WHERE/comparisons par les labels iTop verbatim (~10 fichiers) - Templates badges/filtres: valeurs + labels iTop - itop_service: maintient mapping iTop API internal code <-> DB label - import_sanef_*: norm_etat retourne la valeur iTop verbatim ou None (plus de fallback silencieux sur 'production') Ajoute: - tools/import_etat_itop.py : migration lowercase -> iTop + re-import CSV - tools/import_environnement.py : fix dry-run pour ADD COLUMN idempotent Supprime: - tools/fix_etat_extend.py (obsolete par import_etat_itop.py)
688 lines
30 KiB
Python
688 lines
30 KiB
Python
"""Service QuickWin — gestion des campagnes + exclusions par serveur"""
|
|
import json
|
|
from sqlalchemy import text
|
|
from .quickwin_log_service import log_info, log_warn, log_error, log_success
|
|
|
|
# Exclusions generales par defaut (reboot packages + middleware/apps)
|
|
DEFAULT_GENERAL_EXCLUDES = (
|
|
"dbus* dracut* glibc* grub2* kernel* kexec-tools* "
|
|
"libselinux* linux-firmware* microcode_ctl* mokutil* "
|
|
"net-snmp* NetworkManager* network-scripts* nss* openssl-libs* "
|
|
"polkit* selinux-policy* shim* systemd* tuned*"
|
|
)
|
|
|
|
|
|
def get_server_configs(db, server_ids=None):
|
|
"""Retourne les configs QuickWin pour les serveurs (ou tous)"""
|
|
if server_ids:
|
|
rows = db.execute(text("""
|
|
SELECT qc.*, s.hostname, s.os_family, s.tier,
|
|
d.name as domaine, e.name as environnement,
|
|
z.name as zone
|
|
FROM quickwin_server_config qc
|
|
JOIN servers s ON qc.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 environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
WHERE qc.server_id = ANY(:ids)
|
|
ORDER BY s.hostname
|
|
"""), {"ids": server_ids}).fetchall()
|
|
else:
|
|
rows = db.execute(text("""
|
|
SELECT qc.*, s.hostname, s.os_family, s.tier,
|
|
d.name as domaine, e.name as environnement,
|
|
z.name as zone
|
|
FROM quickwin_server_config qc
|
|
JOIN servers s ON qc.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 environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
ORDER BY s.hostname
|
|
""")).fetchall()
|
|
return rows
|
|
|
|
|
|
def upsert_server_config(db, server_id, general_excludes=None, specific_excludes="", notes=""):
|
|
"""Cree ou met a jour la config QuickWin d'un serveur.
|
|
Si general_excludes est vide lors de la creation, applique DEFAULT_GENERAL_EXCLUDES."""
|
|
existing = db.execute(text(
|
|
"SELECT id FROM quickwin_server_config WHERE server_id = :sid"
|
|
), {"sid": server_id}).fetchone()
|
|
if existing:
|
|
ge = general_excludes if general_excludes is not None else DEFAULT_GENERAL_EXCLUDES
|
|
db.execute(text("""
|
|
UPDATE quickwin_server_config
|
|
SET general_excludes = :ge, specific_excludes = :se, notes = :n, updated_at = now()
|
|
WHERE server_id = :sid
|
|
"""), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes})
|
|
else:
|
|
ge = general_excludes if general_excludes else DEFAULT_GENERAL_EXCLUDES
|
|
db.execute(text("""
|
|
INSERT INTO quickwin_server_config (server_id, general_excludes, specific_excludes, notes)
|
|
VALUES (:sid, :ge, :se, :n)
|
|
"""), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes})
|
|
db.commit()
|
|
|
|
|
|
def delete_server_config(db, config_id):
|
|
db.execute(text("DELETE FROM quickwin_server_config WHERE id = :id"), {"id": config_id})
|
|
db.commit()
|
|
|
|
|
|
def get_eligible_servers(db):
|
|
"""Serveurs Linux production, patch_os_owner=secops"""
|
|
return db.execute(text("""
|
|
SELECT s.id, s.hostname, s.os_family, s.os_version, s.machine_type,
|
|
s.tier, s.etat, s.patch_excludes, s.is_flux_libre, s.is_podman,
|
|
d.name as domaine, d.code as domain_code,
|
|
e.name as environnement, e.code as env_code,
|
|
COALESCE(qc.general_excludes, '') as qw_general_excludes,
|
|
COALESCE(qc.specific_excludes, '') as qw_specific_excludes
|
|
FROM servers s
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN domains d ON de.domain_id = d.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
LEFT JOIN quickwin_server_config qc ON qc.server_id = s.id
|
|
WHERE s.os_family = 'linux'
|
|
AND s.etat = 'Production'
|
|
AND s.patch_os_owner = 'secops'
|
|
ORDER BY e.display_order, d.display_order, s.hostname
|
|
""")).fetchall()
|
|
|
|
|
|
# -- Runs --
|
|
|
|
def list_runs(db):
|
|
return db.execute(text("""
|
|
SELECT r.*,
|
|
u.display_name as created_by_name,
|
|
(SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id) as total_entries,
|
|
(SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'patched') as patched_count,
|
|
(SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'failed') as failed_count,
|
|
(SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'hprod') as hprod_count,
|
|
(SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'prod') as prod_count
|
|
FROM quickwin_runs r
|
|
LEFT JOIN users u ON r.created_by = u.id
|
|
ORDER BY r.year DESC, r.week_number DESC, r.id DESC
|
|
""")).fetchall()
|
|
|
|
|
|
def get_run(db, run_id):
|
|
return db.execute(text("""
|
|
SELECT r.*, u.display_name as created_by_name
|
|
FROM quickwin_runs r LEFT JOIN users u ON r.created_by = u.id
|
|
WHERE r.id = :id
|
|
"""), {"id": run_id}).fetchone()
|
|
|
|
|
|
def get_run_entries(db, run_id):
|
|
return db.execute(text("""
|
|
SELECT qe.*, s.hostname, s.fqdn, s.os_family, s.machine_type,
|
|
d.name as domaine, e.name as environnement
|
|
FROM quickwin_entries qe
|
|
JOIN servers s ON qe.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 environments e ON de.environment_id = e.id
|
|
WHERE qe.run_id = :rid
|
|
ORDER BY qe.branch, s.hostname
|
|
"""), {"rid": run_id}).fetchall()
|
|
|
|
|
|
def create_run(db, year, week_number, label, user_id, server_ids, notes=""):
|
|
"""Cree un run QuickWin avec les serveurs selectionnes.
|
|
Classe auto en hprod/prod selon l'environnement du serveur."""
|
|
row = db.execute(text("""
|
|
INSERT INTO quickwin_runs (year, week_number, label, created_by, notes)
|
|
VALUES (:y, :w, :l, :uid, :n) RETURNING id
|
|
"""), {"y": year, "w": week_number, "l": label, "uid": user_id, "n": notes}).fetchone()
|
|
run_id = row.id
|
|
|
|
# Lire les reboot packages globaux (source: app_secrets)
|
|
from .secrets_service import get_secret
|
|
reboot_pkgs = get_secret(db, "patching_reboot_packages") or DEFAULT_GENERAL_EXCLUDES
|
|
|
|
for sid in server_ids:
|
|
srv = db.execute(text("""
|
|
SELECT s.id, e.name as env_name, COALESCE(s.patch_excludes, '') as pe
|
|
FROM servers s
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
WHERE s.id = :sid
|
|
"""), {"sid": sid}).fetchone()
|
|
if not srv:
|
|
continue
|
|
branch = "prod" if srv.env_name and "production" in srv.env_name.lower() else "hprod"
|
|
# QuickWin : reboot globaux + exclusions iTop du serveur
|
|
db.execute(text("""
|
|
INSERT INTO quickwin_entries (run_id, server_id, branch, general_excludes, specific_excludes)
|
|
VALUES (:rid, :sid, :br, :ge, :se)
|
|
"""), {"rid": run_id, "sid": sid, "br": branch, "ge": reboot_pkgs, "se": srv.pe})
|
|
|
|
db.commit()
|
|
return run_id
|
|
|
|
|
|
def delete_run(db, run_id):
|
|
db.execute(text("DELETE FROM quickwin_entries WHERE run_id = :rid"), {"rid": run_id})
|
|
db.execute(text("DELETE FROM quickwin_runs WHERE id = :rid"), {"rid": run_id})
|
|
db.commit()
|
|
|
|
|
|
def get_available_servers(db, run_id, search="", domains=None, envs=None, zones=None):
|
|
"""Serveurs eligibles PAS encore dans ce run, avec multi-filtres.
|
|
domains/envs/zones: listes de noms (multi-select)."""
|
|
rows = db.execute(text("""
|
|
SELECT s.id, s.hostname, s.os_family, s.machine_type, s.domain_ltd,
|
|
d.name as domaine, e.name as environnement, e.code as env_code,
|
|
z.name as zone
|
|
FROM servers s
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN domains d ON de.domain_id = d.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
WHERE s.os_family = 'linux'
|
|
AND s.etat = 'Production'
|
|
AND s.patch_os_owner = 'secops'
|
|
AND s.id NOT IN (SELECT server_id FROM quickwin_entries WHERE run_id = :rid)
|
|
ORDER BY d.name, e.name, s.hostname
|
|
"""), {"rid": run_id}).fetchall()
|
|
filtered = rows
|
|
if search:
|
|
filtered = [r for r in filtered if search.lower() in r.hostname.lower()]
|
|
if domains:
|
|
filtered = [r for r in filtered if (r.domaine or '') in domains]
|
|
if envs:
|
|
filtered = [r for r in filtered if (r.environnement or '') in envs]
|
|
if zones:
|
|
filtered = [r for r in filtered if (r.zone or '') in zones]
|
|
return filtered
|
|
|
|
|
|
def get_available_filters(db, run_id):
|
|
"""Retourne les domaines, envs et zones disponibles (avec compteurs)."""
|
|
rows = db.execute(text("""
|
|
SELECT d.name as domaine, e.name as environnement, z.name as zone
|
|
FROM servers s
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN domains d ON de.domain_id = d.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
WHERE s.os_family = 'linux'
|
|
AND s.etat = 'Production'
|
|
AND s.patch_os_owner = 'secops'
|
|
AND s.id NOT IN (SELECT server_id FROM quickwin_entries WHERE run_id = :rid)
|
|
"""), {"rid": run_id}).fetchall()
|
|
domains = sorted(set(r.domaine for r in rows if r.domaine))
|
|
envs = sorted(set(r.environnement for r in rows if r.environnement))
|
|
zones = sorted(set(r.zone for r in rows if r.zone))
|
|
# Compteurs
|
|
dom_counts = {}
|
|
for r in rows:
|
|
k = r.domaine or '?'
|
|
dom_counts[k] = dom_counts.get(k, 0) + 1
|
|
env_counts = {}
|
|
for r in rows:
|
|
k = r.environnement or '?'
|
|
env_counts[k] = env_counts.get(k, 0) + 1
|
|
zone_counts = {}
|
|
for r in rows:
|
|
k = r.zone or '?'
|
|
zone_counts[k] = zone_counts.get(k, 0) + 1
|
|
return domains, envs, zones, dom_counts, env_counts, zone_counts
|
|
|
|
|
|
def add_entries_to_run(db, run_id, server_ids, user=None):
|
|
"""Ajoute des serveurs a un run existant. Determine auto hprod/prod.
|
|
Retourne le nombre de serveurs ajoutes."""
|
|
existing = set(r.server_id for r in db.execute(text(
|
|
"SELECT server_id FROM quickwin_entries WHERE run_id = :rid"
|
|
), {"rid": run_id}).fetchall())
|
|
|
|
by = user.get("display_name", user.get("username", "")) if user else ""
|
|
from .secrets_service import get_secret
|
|
reboot_pkgs = get_secret(db, "patching_reboot_packages") or DEFAULT_GENERAL_EXCLUDES
|
|
|
|
added = 0
|
|
hostnames = []
|
|
for sid in server_ids:
|
|
if sid in existing:
|
|
continue
|
|
srv = db.execute(text("""
|
|
SELECT s.id, s.hostname, e.name as env_name, COALESCE(s.patch_excludes, '') as pe
|
|
FROM servers s
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
WHERE s.id = :sid
|
|
"""), {"sid": sid}).fetchone()
|
|
if not srv:
|
|
continue
|
|
branch = "prod" if srv.env_name and "production" in srv.env_name.lower() else "hprod"
|
|
db.execute(text("""
|
|
INSERT INTO quickwin_entries (run_id, server_id, branch, general_excludes, specific_excludes)
|
|
VALUES (:rid, :sid, :br, :ge, :se)
|
|
"""), {"rid": run_id, "sid": sid, "br": branch, "ge": reboot_pkgs, "se": srv.pe})
|
|
added += 1
|
|
hostnames.append(srv.hostname)
|
|
if added:
|
|
log_info(db, run_id, "servers", f"{added} serveur(s) ajoute(s)",
|
|
detail=", ".join(hostnames[:20]) + ("..." if len(hostnames) > 20 else ""),
|
|
created_by=by)
|
|
db.commit()
|
|
return added
|
|
|
|
|
|
def remove_entries_from_run(db, run_id, entry_ids, user=None):
|
|
"""Supprime des entries d'un run. Retourne le nombre de suppressions."""
|
|
if not entry_ids:
|
|
return 0
|
|
by = user.get("display_name", user.get("username", "")) if user else ""
|
|
# Log hostnames avant suppression
|
|
rows = db.execute(text("""
|
|
SELECT s.hostname FROM quickwin_entries qe
|
|
JOIN servers s ON qe.server_id = s.id
|
|
WHERE qe.run_id = :rid AND qe.id = ANY(:ids)
|
|
"""), {"rid": run_id, "ids": entry_ids}).fetchall()
|
|
hostnames = [r.hostname for r in rows]
|
|
result = db.execute(text(
|
|
"DELETE FROM quickwin_entries WHERE run_id = :rid AND id = ANY(:ids)"
|
|
), {"rid": run_id, "ids": entry_ids})
|
|
removed = result.rowcount
|
|
if removed:
|
|
log_warn(db, run_id, "servers", f"{removed} serveur(s) supprime(s)",
|
|
detail=", ".join(hostnames[:20]) + ("..." if len(hostnames) > 20 else ""),
|
|
created_by=by)
|
|
db.commit()
|
|
return removed
|
|
|
|
|
|
def get_campaign_scope(db, run_id):
|
|
"""Retourne domaines, envs et zones presents dans le run avec compteurs."""
|
|
rows = db.execute(text("""
|
|
SELECT d.name as domaine, e.name as environnement, z.name as zone, qe.status
|
|
FROM quickwin_entries qe
|
|
JOIN servers s ON qe.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 environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
WHERE qe.run_id = :rid
|
|
"""), {"rid": run_id}).fetchall()
|
|
dom_counts = {}
|
|
env_counts = {}
|
|
zone_counts = {}
|
|
dom_active = {} # non-excluded count
|
|
zone_active = {} # non-excluded count
|
|
for r in rows:
|
|
d = r.domaine or '?'
|
|
e = r.environnement or '?'
|
|
z = r.zone or '?'
|
|
dom_counts[d] = dom_counts.get(d, 0) + 1
|
|
env_counts[e] = env_counts.get(e, 0) + 1
|
|
zone_counts[z] = zone_counts.get(z, 0) + 1
|
|
if r.status != 'excluded':
|
|
dom_active[d] = dom_active.get(d, 0) + 1
|
|
zone_active[z] = zone_active.get(z, 0) + 1
|
|
domains = sorted(k for k in dom_counts if k != '?')
|
|
envs = sorted(k for k in env_counts if k != '?')
|
|
zones = sorted(k for k in zone_counts if k != '?')
|
|
return {
|
|
"domains": domains, "envs": envs, "zones": zones,
|
|
"dom_counts": dom_counts, "env_counts": env_counts, "zone_counts": zone_counts,
|
|
"dom_active": dom_active, "zone_active": zone_active,
|
|
}
|
|
|
|
|
|
def apply_scope(db, run_id, keep_domains=None, keep_zones=None, user=None):
|
|
"""Applique le perimetre: serveurs hors domaines/zones selectionnes -> excluded.
|
|
Serveurs dans le perimetre -> pending (si etaient excluded)."""
|
|
by = user.get("display_name", user.get("username", "")) if user else ""
|
|
|
|
# Recuperer toutes les entries avec leur domaine/zone
|
|
entries = db.execute(text("""
|
|
SELECT qe.id, qe.status, s.hostname, d.name as domaine, z.name as zone
|
|
FROM quickwin_entries qe
|
|
JOIN servers s ON qe.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 qe.run_id = :rid
|
|
"""), {"rid": run_id}).fetchall()
|
|
|
|
included = 0
|
|
excluded = 0
|
|
for e in entries:
|
|
in_scope = True
|
|
if keep_domains and (e.domaine or '') not in keep_domains:
|
|
in_scope = False
|
|
if keep_zones and (e.zone or '') not in keep_zones:
|
|
in_scope = False
|
|
|
|
if in_scope and e.status == 'excluded':
|
|
db.execute(text("UPDATE quickwin_entries SET status='pending', updated_at=now() WHERE id=:id"),
|
|
{"id": e.id})
|
|
included += 1
|
|
elif not in_scope and e.status != 'excluded':
|
|
db.execute(text("UPDATE quickwin_entries SET status='excluded', updated_at=now() WHERE id=:id"),
|
|
{"id": e.id})
|
|
excluded += 1
|
|
|
|
scope_desc = []
|
|
if keep_domains:
|
|
scope_desc.append(f"domaines: {', '.join(keep_domains)}")
|
|
if keep_zones:
|
|
scope_desc.append(f"zones: {', '.join(keep_zones)}")
|
|
log_info(db, run_id, "scope",
|
|
f"Perimetre applique: {included} inclus, {excluded} exclus",
|
|
detail=" | ".join(scope_desc) if scope_desc else "Tous",
|
|
created_by=by)
|
|
db.commit()
|
|
return included, excluded
|
|
|
|
|
|
def update_entry_status(db, entry_id, status, patch_output="", packages_count=0,
|
|
packages="", reboot_required=False, notes=""):
|
|
db.execute(text("""
|
|
UPDATE quickwin_entries SET
|
|
status = :st, patch_output = :po, patch_packages_count = :pc,
|
|
patch_packages = :pp, reboot_required = :rb, notes = :n,
|
|
patch_date = CASE WHEN :st IN ('patched','failed') THEN now() ELSE patch_date END,
|
|
updated_at = now()
|
|
WHERE id = :id
|
|
"""), {"id": entry_id, "st": status, "po": patch_output, "pc": packages_count,
|
|
"pp": packages, "rb": reboot_required, "n": notes})
|
|
db.commit()
|
|
|
|
# Création auto d'une entrée patch_validation (en_attente) pour les serveurs patchés
|
|
if status == "patched":
|
|
row = db.execute(text("SELECT server_id, run_id FROM quickwin_entries WHERE id=:id"),
|
|
{"id": entry_id}).fetchone()
|
|
if row:
|
|
# Éviter les doublons (même run + même server dans la dernière heure)
|
|
existing = db.execute(text("""SELECT id FROM patch_validation
|
|
WHERE server_id=:sid AND campaign_id=:cid AND campaign_type='quickwin'
|
|
AND patch_date >= NOW() - INTERVAL '1 hour'"""),
|
|
{"sid": row.server_id, "cid": row.run_id}).fetchone()
|
|
if not existing:
|
|
db.execute(text("""INSERT INTO patch_validation (server_id, campaign_id,
|
|
campaign_type, patch_date, status)
|
|
VALUES (:sid, :cid, 'quickwin', NOW(), 'en_attente')"""),
|
|
{"sid": row.server_id, "cid": row.run_id})
|
|
db.commit()
|
|
|
|
|
|
def update_entry_field(db, entry_id, field, value):
|
|
"""Mise a jour d'un champ unique (pour inline edit)"""
|
|
allowed = ("general_excludes", "specific_excludes", "notes", "status",
|
|
"snap_done", "prereq_ok", "prereq_detail", "dryrun_output")
|
|
if field not in allowed:
|
|
return False
|
|
db.execute(text(f"UPDATE quickwin_entries SET {field} = :val, updated_at = now() WHERE id = :id"),
|
|
{"val": value, "id": entry_id})
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def can_start_prod(db, run_id):
|
|
"""Verifie que tous les hprod sont termines avant d'autoriser le prod"""
|
|
pending = db.execute(text("""
|
|
SELECT COUNT(*) as cnt FROM quickwin_entries
|
|
WHERE run_id = :rid AND branch = 'hprod' AND status IN ('pending', 'in_progress')
|
|
"""), {"rid": run_id}).fetchone()
|
|
return pending.cnt == 0
|
|
|
|
|
|
def check_prod_validations(db, run_id):
|
|
"""Vérifie que chaque prod du run a ses non-prod liés validés (via server_correspondance + patch_validation).
|
|
Retourne (ok, blockers) où blockers = liste [{prod_hostname, nonprod_hostname, status}].
|
|
Ignore les prods sans non-prod lié (OK par défaut)."""
|
|
rows = db.execute(text("""
|
|
SELECT qe.id as entry_id, ps.id as prod_id, ps.hostname as prod_host
|
|
FROM quickwin_entries qe JOIN servers ps ON qe.server_id = ps.id
|
|
WHERE qe.run_id = :rid AND qe.branch = 'prod' AND qe.status NOT IN ('excluded','skipped','patched')
|
|
"""), {"rid": run_id}).fetchall()
|
|
|
|
blockers = []
|
|
for r in rows:
|
|
corrs = db.execute(text("""SELECT ns.hostname,
|
|
(SELECT pv.status FROM patch_validation pv WHERE pv.server_id = ns.id
|
|
ORDER BY pv.patch_date DESC LIMIT 1) as last_status
|
|
FROM server_correspondance sc JOIN servers ns ON sc.nonprod_server_id = ns.id
|
|
WHERE sc.prod_server_id = :pid"""), {"pid": r.prod_id}).fetchall()
|
|
for c in corrs:
|
|
if c.last_status not in ("validated_ok", "forced"):
|
|
blockers.append({"prod_hostname": r.prod_host,
|
|
"nonprod_hostname": c.hostname,
|
|
"status": c.last_status or "aucun_patching"})
|
|
return (len(blockers) == 0), blockers
|
|
|
|
|
|
def get_run_stats(db, run_id):
|
|
return db.execute(text("""
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE status != 'excluded') as total,
|
|
COUNT(*) FILTER (WHERE branch = 'hprod' AND status != 'excluded') as hprod_total,
|
|
COUNT(*) FILTER (WHERE branch = 'prod' AND status != 'excluded') as prod_total,
|
|
COUNT(*) FILTER (WHERE status = 'patched') as patched,
|
|
COUNT(*) FILTER (WHERE status = 'failed') as failed,
|
|
COUNT(*) FILTER (WHERE status = 'pending') as pending,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND branch = 'hprod') as hprod_pending,
|
|
COUNT(*) FILTER (WHERE status = 'excluded') as excluded,
|
|
COUNT(*) FILTER (WHERE status = 'skipped') as skipped,
|
|
COUNT(*) FILTER (WHERE branch = 'hprod' AND status = 'patched') as hprod_patched,
|
|
COUNT(*) FILTER (WHERE branch = 'prod' AND status = 'patched') as prod_patched,
|
|
COUNT(*) FILTER (WHERE reboot_required AND status != 'excluded') as reboot_count
|
|
FROM quickwin_entries WHERE run_id = :rid
|
|
"""), {"rid": run_id}).fetchone()
|
|
|
|
|
|
def advance_run_status(db, run_id, target_status, user=None):
|
|
"""Avance le statut du run vers l'etape suivante"""
|
|
VALID_TRANSITIONS = {
|
|
"draft": "prereq",
|
|
"prereq": "snapshot",
|
|
"snapshot": "patching",
|
|
"patching": "result",
|
|
"result": "completed",
|
|
}
|
|
run = db.execute(text("SELECT status FROM quickwin_runs WHERE id = :id"),
|
|
{"id": run_id}).fetchone()
|
|
if not run:
|
|
return False
|
|
by = user.get("display_name", user.get("username", "")) if user else ""
|
|
if target_status == "draft":
|
|
db.execute(text("UPDATE quickwin_runs SET status = :st, updated_at = now() WHERE id = :id"),
|
|
{"st": target_status, "id": run_id})
|
|
log_info(db, run_id, "workflow", f"Retour a l'etape brouillon", created_by=by)
|
|
db.commit()
|
|
return True
|
|
expected = VALID_TRANSITIONS.get(run.status)
|
|
if expected != target_status:
|
|
log_warn(db, run_id, "workflow",
|
|
f"Transition refusee: {run.status} -> {target_status}", created_by=by)
|
|
db.commit()
|
|
return False
|
|
db.execute(text("UPDATE quickwin_runs SET status = :st, updated_at = now() WHERE id = :id"),
|
|
{"st": target_status, "id": run_id})
|
|
log_info(db, run_id, "workflow", f"Passage a l'etape: {target_status}", created_by=by)
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def get_step_stats(db, run_id, branch=None):
|
|
"""Stats par etape pour un run (optionnel: filtre par branch)"""
|
|
where = "run_id = :rid"
|
|
params = {"rid": run_id}
|
|
if branch:
|
|
where += " AND branch = :br"
|
|
params["br"] = branch
|
|
return db.execute(text(f"""
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE status NOT IN ('excluded','skipped')) as active,
|
|
COUNT(*) FILTER (WHERE prereq_ok = true) as prereq_ok,
|
|
COUNT(*) FILTER (WHERE prereq_ok = false) as prereq_ko,
|
|
COUNT(*) FILTER (WHERE prereq_ok IS NULL AND status NOT IN ('excluded','skipped')) as prereq_pending,
|
|
COUNT(*) FILTER (WHERE snap_done = true) as snap_ok,
|
|
COUNT(*) FILTER (WHERE snap_done = false AND status NOT IN ('excluded','skipped')) as snap_pending,
|
|
COUNT(*) FILTER (WHERE status = 'patched') as patched,
|
|
COUNT(*) FILTER (WHERE status = 'failed') as failed,
|
|
COUNT(*) FILTER (WHERE status = 'pending') as pending,
|
|
COUNT(*) FILTER (WHERE reboot_required = true) as reboot_count
|
|
FROM quickwin_entries WHERE {where}
|
|
"""), params).fetchone()
|
|
|
|
|
|
def check_prereqs(db, run_id, branch, user=None):
|
|
"""Lance les verifications prerequis pour tous les serveurs d'une branche.
|
|
Etapes par serveur: DNS resolution, SSH, espace disque, satellite."""
|
|
from .quickwin_prereq_service import check_server_prereqs
|
|
|
|
entries = db.execute(text("""
|
|
SELECT qe.id, s.hostname, s.domain_ltd, s.ssh_method,
|
|
e.code as env_code
|
|
FROM quickwin_entries qe
|
|
JOIN servers s ON qe.server_id = s.id
|
|
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
|
LEFT JOIN environments e ON de.environment_id = e.id
|
|
WHERE qe.run_id = :rid AND qe.branch = :br
|
|
AND qe.status NOT IN ('excluded','skipped')
|
|
ORDER BY s.hostname
|
|
"""), {"rid": run_id, "br": branch}).fetchall()
|
|
|
|
by = user.get("display_name", user.get("username", "")) if user else ""
|
|
log_info(db, run_id, "prereq",
|
|
f"Lancement check prerequis {branch} ({len(entries)} serveurs)", created_by=by)
|
|
|
|
results = []
|
|
ok_count = 0
|
|
for e in entries:
|
|
try:
|
|
r = check_server_prereqs(
|
|
hostname=e.hostname,
|
|
db=db,
|
|
domain_ltd=e.domain_ltd,
|
|
env_code=e.env_code,
|
|
ssh_method=e.ssh_method or "ssh_key",
|
|
)
|
|
except Exception as ex:
|
|
r = {"dns_ok": False, "ssh_ok": False, "satellite_ok": False,
|
|
"disk_ok": False, "detail": str(ex), "skip": True}
|
|
|
|
prereq_ok = (r.get("dns_ok", False) and r.get("ssh_ok", False)
|
|
and r.get("satellite_ok", False) and r.get("disk_ok", True))
|
|
db.execute(text("""
|
|
UPDATE quickwin_entries SET
|
|
prereq_ok = :ok, prereq_ssh = :ssh, prereq_satellite = :sat, prereq_disk = :disk,
|
|
prereq_detail = :detail, prereq_date = now(), updated_at = now()
|
|
WHERE id = :id
|
|
"""), {
|
|
"id": e.id, "ok": prereq_ok,
|
|
"ssh": r.get("ssh_ok", False), "sat": r.get("satellite_ok", False),
|
|
"disk": r.get("disk_ok", True), "detail": r.get("detail", ""),
|
|
})
|
|
# Log par serveur
|
|
detail_str = r.get("detail", "")
|
|
if prereq_ok:
|
|
ok_count += 1
|
|
log_success(db, run_id, "prereq", f"OK: {e.hostname}",
|
|
detail=detail_str, entry_id=e.id, hostname=e.hostname)
|
|
else:
|
|
log_error(db, run_id, "prereq", f"KO: {e.hostname}",
|
|
detail=detail_str, entry_id=e.id, hostname=e.hostname)
|
|
results.append({"hostname": e.hostname, "ok": prereq_ok, "detail": detail_str})
|
|
|
|
ko_count = len(results) - ok_count
|
|
log_info(db, run_id, "prereq",
|
|
f"Fin check {branch}: {ok_count} OK, {ko_count} KO sur {len(results)}", created_by=by)
|
|
db.commit()
|
|
return results
|
|
|
|
|
|
def mark_snapshot(db, entry_id, done=True):
|
|
"""Marque un serveur comme snapshot fait/pas fait"""
|
|
db.execute(text("""
|
|
UPDATE quickwin_entries SET snap_done = :done,
|
|
snap_date = CASE WHEN :done THEN now() ELSE NULL END,
|
|
updated_at = now() WHERE id = :id
|
|
"""), {"id": entry_id, "done": done})
|
|
db.commit()
|
|
|
|
|
|
def mark_all_snapshots(db, run_id, branch, done=True):
|
|
"""Marque tous les serveurs d'une branche comme snapshot fait"""
|
|
db.execute(text("""
|
|
UPDATE quickwin_entries SET snap_done = :done,
|
|
snap_date = CASE WHEN :done THEN now() ELSE NULL END,
|
|
updated_at = now()
|
|
WHERE run_id = :rid AND branch = :br AND status NOT IN ('excluded','skipped')
|
|
"""), {"rid": run_id, "br": branch, "done": done})
|
|
db.commit()
|
|
|
|
|
|
def build_yum_commands(db, run_id, branch):
|
|
"""Construit les commandes yum pour chaque serveur d'une branche.
|
|
Inclut tous les serveurs actifs (non excluded/skipped) avec snap fait."""
|
|
entries = db.execute(text("""
|
|
SELECT qe.id, s.hostname, qe.general_excludes, qe.specific_excludes
|
|
FROM quickwin_entries qe
|
|
JOIN servers s ON qe.server_id = s.id
|
|
WHERE qe.run_id = :rid AND qe.branch = :br
|
|
AND qe.status NOT IN ('excluded','skipped')
|
|
AND qe.snap_done = true
|
|
ORDER BY s.hostname
|
|
"""), {"rid": run_id, "br": branch}).fetchall()
|
|
|
|
commands = []
|
|
for e in entries:
|
|
excludes = (e.general_excludes or "") + " " + (e.specific_excludes or "")
|
|
parts = [p.strip() for p in excludes.split() if p.strip()]
|
|
exclude_args = " ".join(f"--exclude={p}" for p in parts)
|
|
cmd = f"yum update -y {exclude_args}".strip()
|
|
db.execute(text("UPDATE quickwin_entries SET patch_command = :cmd WHERE id = :id"),
|
|
{"cmd": cmd, "id": e.id})
|
|
commands.append({"id": e.id, "hostname": e.hostname, "command": cmd})
|
|
db.commit()
|
|
return commands
|
|
|
|
|
|
def inject_yum_history(db, data):
|
|
"""Injecte l'historique yum dans quickwin_server_config.
|
|
data = [{"server": "hostname", "yum_commands": [...]}]"""
|
|
updated = 0
|
|
inserted = 0
|
|
for item in data:
|
|
hostname = item.get("server", item.get("server_name", "")).strip()
|
|
if not hostname:
|
|
continue
|
|
srv = db.execute(text("SELECT id FROM servers WHERE hostname = :h"), {"h": hostname}).fetchone()
|
|
if not srv:
|
|
continue
|
|
cmds = json.dumps(item.get("yum_commands", item.get("last_yum_commands", [])), ensure_ascii=False)
|
|
existing = db.execute(text(
|
|
"SELECT id FROM quickwin_server_config WHERE server_id = :sid"
|
|
), {"sid": srv.id}).fetchone()
|
|
if existing:
|
|
db.execute(text("""
|
|
UPDATE quickwin_server_config SET last_yum_commands = :cmds::jsonb, updated_at = now()
|
|
WHERE server_id = :sid
|
|
"""), {"sid": srv.id, "cmds": cmds})
|
|
updated += 1
|
|
else:
|
|
db.execute(text("""
|
|
INSERT INTO quickwin_server_config (server_id, last_yum_commands)
|
|
VALUES (:sid, :cmds::jsonb)
|
|
"""), {"sid": srv.id, "cmds": cmds})
|
|
inserted += 1
|
|
db.commit()
|
|
return updated, inserted
|
|
|
|
|
|
# Correspondance HPROD ↔ PROD : logique déplacée vers server_correspondance (global)
|
|
# Les fonctions obsolètes ont été supprimées : compute_correspondance, get_correspondance,
|
|
# get_available_prod_entries, set_prod_pair, clear_all_pairs.
|
|
# La colonne prod_pair_entry_id de quickwin_entries est laissée en place pour compatibilité
|
|
# mais n'est plus utilisée. Les liens sont désormais dans server_correspondance.
|