- BOC SAP: stop_order ajouté (SolMan→CM→CC→AS→CI→HANA), conforme doc v3.1.4 - 3 serveurs BOC HO ajoutés (vpbocarep1, vpbocasec1, vpbocjump1) - Patch waves: DNS (V1: 1+3, V2: 2+4), SMTP (V1: smtp2, V2: smtp1) - Colonnes Stop order / Start order dans Spécifiques - Campagne: colonne Zone (DMZ rouge, EMV jaune, LAN bleu) + KPI DMZ - DMZ: filtre par zone au lieu de domaine (27 serveurs récupérés) - Préférences patching: jour/heure éditables dans serveurs, hérités en campagne - KPIs campagne en flex (une seule ligne) - Limites intervenants: layout compact (max-width 400px) - Tri campagne: domaine → hors-prod/prod → hostname - Opérateur peut prendre en in_progress + planned - Actions bulk campagne: prendre/assigner/exclure en masse - Formulaires inline: fix Alpine.js → JS pur (display:none par défaut) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
523 lines
22 KiB
Python
523 lines
22 KiB
Python
"""Service campagnes — logique metier patching"""
|
|
from datetime import datetime, date, timedelta
|
|
from sqlalchemy import text
|
|
|
|
|
|
def list_campaigns(db, year=None, status=None):
|
|
where = ["1=1"]
|
|
params = {}
|
|
if year:
|
|
where.append("c.year = :year"); params["year"] = year
|
|
if status:
|
|
where.append("c.status = :status"); params["status"] = status
|
|
wc = " AND ".join(where)
|
|
return db.execute(text(f"""
|
|
SELECT c.*, u.display_name as created_by_name,
|
|
(SELECT COUNT(*) FROM patch_sessions ps WHERE ps.campaign_id = c.id) as session_count,
|
|
(SELECT COUNT(*) FROM patch_sessions ps WHERE ps.campaign_id = c.id AND ps.status = 'patched') as patched_count,
|
|
(SELECT COUNT(*) FROM patch_sessions ps WHERE ps.campaign_id = c.id AND ps.status = 'failed') as failed_count,
|
|
(SELECT COUNT(*) FROM patch_sessions ps WHERE ps.campaign_id = c.id AND ps.status = 'pending') as pending_count,
|
|
(SELECT COUNT(*) FROM patch_sessions ps WHERE ps.campaign_id = c.id AND ps.status = 'excluded') as excluded_count
|
|
FROM campaigns c
|
|
LEFT JOIN users u ON c.created_by = u.id
|
|
WHERE {wc} ORDER BY c.year DESC, c.week_code DESC
|
|
"""), params).fetchall()
|
|
|
|
|
|
def get_campaign(db, campaign_id):
|
|
return db.execute(text("""
|
|
SELECT c.*, u.display_name as created_by_name
|
|
FROM campaigns c LEFT JOIN users u ON c.created_by = u.id
|
|
WHERE c.id = :id
|
|
"""), {"id": campaign_id}).fetchone()
|
|
|
|
|
|
def get_campaign_sessions(db, campaign_id):
|
|
return db.execute(text("""
|
|
SELECT ps.*, s.hostname, s.fqdn, s.os_family, s.os_version, s.tier,
|
|
s.etat, s.ssh_method, s.licence_support, s.machine_type,
|
|
s.pref_patch_jour, s.pref_patch_heure,
|
|
d.name as domaine, e.name as environnement, z.name as zone_name,
|
|
u.display_name as intervenant_name
|
|
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 environments e ON de.environment_id = e.id
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
LEFT JOIN users u ON ps.intervenant_id = u.id
|
|
WHERE ps.campaign_id = :cid
|
|
ORDER BY CASE ps.status
|
|
WHEN 'in_progress' THEN 1 WHEN 'pending' THEN 2 WHEN 'prereq_ok' THEN 3
|
|
WHEN 'patched' THEN 4 WHEN 'failed' THEN 5 WHEN 'reported' THEN 6
|
|
WHEN 'excluded' THEN 7 WHEN 'cancelled' THEN 8 ELSE 9 END,
|
|
d.name,
|
|
CASE WHEN e.name != 'Production' THEN 0 ELSE 1 END,
|
|
s.hostname
|
|
"""), {"cid": campaign_id}).fetchall()
|
|
|
|
|
|
def get_campaign_stats(db, campaign_id):
|
|
return db.execute(text("""
|
|
SELECT
|
|
COUNT(*) as 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 = 'in_progress') as in_progress,
|
|
COUNT(*) FILTER (WHERE status = 'skipped') as skipped,
|
|
COUNT(*) FILTER (WHERE status = 'excluded') as excluded,
|
|
COUNT(*) FILTER (WHERE status = 'reported') as reported,
|
|
COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled,
|
|
COUNT(*) FILTER (WHERE intervenant_id IS NOT NULL AND status NOT IN ('excluded','cancelled')) as assigned,
|
|
COUNT(*) FILTER (WHERE intervenant_id IS NULL AND status NOT IN ('excluded','cancelled')) as unassigned,
|
|
COUNT(*) FILTER (WHERE ps.server_id IN (SELECT s2.id FROM servers s2 JOIN zones z2 ON s2.zone_id = z2.id WHERE z2.name = 'DMZ') AND status NOT IN ('excluded','cancelled')) as dmz
|
|
FROM patch_sessions ps WHERE ps.campaign_id = :cid
|
|
"""), {"cid": campaign_id}).fetchone()
|
|
|
|
|
|
def get_planning_for_week(db, year, week_number):
|
|
return db.execute(text("""
|
|
SELECT pp.*, d.name as domain_name
|
|
FROM patch_planning pp
|
|
LEFT JOIN domains d ON pp.domain_code = d.code
|
|
WHERE pp.year = :y AND pp.week_number = :wn AND pp.status = 'open'
|
|
ORDER BY pp.domain_code
|
|
"""), {"y": year, "wn": week_number}).fetchall()
|
|
|
|
|
|
def _week_dates(year, week_number):
|
|
"""Retourne lun, mar, mer, jeu de la semaine ISO"""
|
|
jan4 = date(year, 1, 4)
|
|
start_of_w1 = jan4 - timedelta(days=jan4.isoweekday() - 1)
|
|
monday = start_of_w1 + timedelta(weeks=week_number - 1)
|
|
return monday, monday + timedelta(1), monday + timedelta(2), monday + timedelta(3)
|
|
|
|
|
|
def get_servers_for_planning(db, year, week_number):
|
|
planning = get_planning_for_week(db, year, week_number)
|
|
if not planning:
|
|
return [], []
|
|
|
|
domain_envs = []
|
|
for p in planning:
|
|
if p.domain_code == 'DMZ':
|
|
continue
|
|
if p.env_scope == 'prod':
|
|
domain_envs.append(("d.code = :dc_{0} AND e.name = 'Production'".format(len(domain_envs)), p.domain_code))
|
|
elif p.env_scope == 'hprod':
|
|
domain_envs.append(("d.code = :dc_{0} AND e.name != 'Production'".format(len(domain_envs)), p.domain_code))
|
|
elif p.env_scope == 'prod_pilot':
|
|
domain_envs.append(("d.code = :dc_{0}".format(len(domain_envs)), p.domain_code))
|
|
else:
|
|
domain_envs.append(("d.code = :dc_{0}".format(len(domain_envs)), p.domain_code))
|
|
|
|
if not domain_envs:
|
|
return [], planning
|
|
|
|
or_clauses = []
|
|
params = {}
|
|
for i, (clause, dc) in enumerate(domain_envs):
|
|
or_clauses.append(clause)
|
|
params[f"dc_{i}"] = dc
|
|
# DMZ = par zone, pas par domaine
|
|
or_clauses.append("z.name = 'DMZ'")
|
|
|
|
where = f"""
|
|
s.etat = 'en_production' AND s.patch_os_owner = 'secops'
|
|
AND s.licence_support IN ('active', 'els') AND s.os_family = 'linux'
|
|
AND ({' OR '.join(or_clauses)})
|
|
"""
|
|
|
|
servers = db.execute(text(f"""
|
|
SELECT s.id, s.hostname, s.fqdn, s.os_family, s.os_version, s.tier,
|
|
s.licence_support, s.ssh_method, s.machine_type,
|
|
s.pref_patch_jour, s.pref_patch_heure, s.default_intervenant_id, s.app_group,
|
|
d.name as domaine, d.code as domain_code, e.name as environnement
|
|
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 {where}
|
|
ORDER BY e.name, d.name, s.hostname
|
|
"""), params).fetchall()
|
|
|
|
return servers, planning
|
|
|
|
|
|
def _generate_slots():
|
|
"""Genere les creneaux horaires de la journee: 09h00-12h30 + 14h00-16h45 par pas de 15min"""
|
|
slots = []
|
|
# Matin: 09h00 a 12h15 (dernier debut = 12h15, fin 12h30)
|
|
h, m = 9, 0
|
|
while h < 12 or (h == 12 and m <= 15):
|
|
slots.append(f"{h:02d}h{m:02d}")
|
|
m += 15
|
|
if m >= 60:
|
|
m = 0; h += 1
|
|
# Apres-midi: 14h00 a 16h30 (dernier debut = 16h30, fin 16h45)
|
|
h, m = 14, 0
|
|
while h < 16 or (h == 16 and m <= 30):
|
|
slots.append(f"{h:02d}h{m:02d}")
|
|
m += 15
|
|
if m >= 60:
|
|
m = 0; h += 1
|
|
return slots
|
|
|
|
|
|
DAILY_SLOTS = _generate_slots() # 27 slots par jour
|
|
|
|
|
|
def create_campaign_from_planning(db, year, week_number, label, user_id, excluded_ids=None):
|
|
servers, planning = get_servers_for_planning(db, year, week_number)
|
|
if not servers:
|
|
return None
|
|
|
|
wc = f"S{week_number:02d}"
|
|
lun, mar, mer, jeu = _week_dates(year, week_number)
|
|
|
|
row = db.execute(text("""
|
|
INSERT INTO campaigns (week_code, year, label, status, date_start, date_end, created_by)
|
|
VALUES (:wc, :y, :label, 'draft', :ds, :de, :uid)
|
|
RETURNING id
|
|
"""), {"wc": wc, "y": year, "label": label, "ds": lun, "de": jeu, "uid": user_id}).fetchone()
|
|
cid = row.id
|
|
excluded = set(excluded_ids or [])
|
|
|
|
# Separer hors-prod et prod
|
|
hprod_servers = [s for s in servers if s.environnement != 'Production' and s.id not in excluded]
|
|
prod_servers = [s for s in servers if s.environnement == 'Production' and s.id not in excluded]
|
|
excluded_servers = [s for s in servers if s.id in excluded]
|
|
|
|
# Trier par app_group pour grouper les serveurs du meme applicatif
|
|
hprod_servers.sort(key=lambda s: (s.app_group or '', s.hostname))
|
|
prod_servers.sort(key=lambda s: (s.app_group or '', s.hostname))
|
|
|
|
# Attribuer les creneaux
|
|
# Hors-prod: lundi + mardi
|
|
hprod_jours = [lun, mar]
|
|
slot_idx = 0
|
|
jour_idx = 0
|
|
for s in hprod_servers:
|
|
jour = hprod_jours[jour_idx]
|
|
heure = DAILY_SLOTS[slot_idx]
|
|
|
|
# Preference serveur
|
|
if s.pref_patch_jour and s.pref_patch_jour != 'indifferent':
|
|
jour_map = {"lundi": lun, "mardi": mar}
|
|
jour = jour_map.get(s.pref_patch_jour, jour)
|
|
if s.pref_patch_heure and s.pref_patch_heure != 'indifferent':
|
|
heure = s.pref_patch_heure
|
|
|
|
default_op = s.default_intervenant_id if hasattr(s, 'default_intervenant_id') else None
|
|
forced = True if default_op else False
|
|
|
|
db.execute(text("""
|
|
INSERT INTO patch_sessions (campaign_id, server_id, status, date_prevue, heure_prevue,
|
|
intervenant_id, forced_assignment, assigned_at)
|
|
VALUES (:cid, :sid, 'pending', :dp, :hp, :oid, :forced, CASE WHEN :oid IS NOT NULL THEN now() END)
|
|
ON CONFLICT (campaign_id, server_id) DO NOTHING
|
|
"""), {"cid": cid, "sid": s.id, "dp": jour, "hp": heure,
|
|
"oid": default_op, "forced": forced})
|
|
|
|
slot_idx += 1
|
|
if slot_idx >= len(DAILY_SLOTS):
|
|
slot_idx = 0
|
|
jour_idx = min(jour_idx + 1, len(hprod_jours) - 1)
|
|
|
|
# Prod: mercredi + jeudi
|
|
prod_jours = [mer, jeu]
|
|
slot_idx = 0
|
|
jour_idx = 0
|
|
for s in prod_servers:
|
|
jour = prod_jours[jour_idx]
|
|
heure = DAILY_SLOTS[slot_idx]
|
|
|
|
if s.pref_patch_jour and s.pref_patch_jour != 'indifferent':
|
|
jour_map = {"mercredi": mer, "jeudi": jeu}
|
|
jour = jour_map.get(s.pref_patch_jour, jour)
|
|
if s.pref_patch_heure and s.pref_patch_heure != 'indifferent':
|
|
heure = s.pref_patch_heure
|
|
|
|
default_op = s.default_intervenant_id if hasattr(s, 'default_intervenant_id') else None
|
|
forced = True if default_op else False
|
|
|
|
db.execute(text("""
|
|
INSERT INTO patch_sessions (campaign_id, server_id, status, date_prevue, heure_prevue,
|
|
intervenant_id, forced_assignment, assigned_at)
|
|
VALUES (:cid, :sid, 'pending', :dp, :hp, :oid, :forced, CASE WHEN :oid IS NOT NULL THEN now() END)
|
|
ON CONFLICT (campaign_id, server_id) DO NOTHING
|
|
"""), {"cid": cid, "sid": s.id, "dp": jour, "hp": heure,
|
|
"oid": default_op, "forced": forced})
|
|
|
|
slot_idx += 1
|
|
if slot_idx >= len(DAILY_SLOTS):
|
|
slot_idx = 0
|
|
jour_idx = min(jour_idx + 1, len(prod_jours) - 1)
|
|
|
|
# Exclus
|
|
for s in excluded_servers:
|
|
db.execute(text("""
|
|
INSERT INTO patch_sessions (campaign_id, server_id, status)
|
|
VALUES (:cid, :sid, 'excluded')
|
|
ON CONFLICT (campaign_id, server_id) DO NOTHING
|
|
"""), {"cid": cid, "sid": s.id})
|
|
|
|
# Appliquer les regles d'assignation par defaut puis propager par app_group
|
|
_apply_default_assignments(db, cid)
|
|
|
|
count = db.execute(text(
|
|
"SELECT COUNT(*) FROM patch_sessions WHERE campaign_id = :cid AND status != 'excluded'"
|
|
), {"cid": cid}).scalar()
|
|
db.execute(text("UPDATE campaigns SET total_servers = :c WHERE id = :cid"),
|
|
{"c": count, "cid": cid})
|
|
|
|
db.commit()
|
|
return cid
|
|
|
|
|
|
def _apply_default_assignments(db, campaign_id):
|
|
"""Applique les regles d'assignation par defaut (table default_assignments).
|
|
Priorite: server > app_type > app_group > domain > zone"""
|
|
rules = db.execute(text("""
|
|
SELECT da.rule_type, da.rule_value, da.user_id
|
|
FROM default_assignments da
|
|
JOIN users u ON da.user_id = u.id AND u.is_active = true
|
|
ORDER BY da.priority ASC, da.rule_type
|
|
""")).fetchall()
|
|
|
|
for rule in rules:
|
|
if rule.rule_type == 'server':
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :uid, forced_assignment = true, assigned_at = now()
|
|
FROM servers s
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND LOWER(s.hostname) = LOWER(:val)
|
|
AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "uid": rule.user_id, "val": rule.rule_value})
|
|
|
|
elif rule.rule_type == 'app_type':
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :uid, forced_assignment = true, assigned_at = now()
|
|
FROM servers s
|
|
LEFT JOIN server_specifics ss ON ss.server_id = s.id
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND UPPER(ss.app_type) = UPPER(:val)
|
|
AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "uid": rule.user_id, "val": rule.rule_value})
|
|
|
|
elif rule.rule_type == 'app_group':
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :uid, forced_assignment = true, assigned_at = now()
|
|
FROM servers s
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND s.app_group = :val
|
|
AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "uid": rule.user_id, "val": rule.rule_value})
|
|
|
|
elif rule.rule_type == 'domain':
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :uid, forced_assignment = true, assigned_at = now()
|
|
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
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND d.code = :val
|
|
AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "uid": rule.user_id, "val": rule.rule_value})
|
|
|
|
elif rule.rule_type == 'zone':
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :uid, forced_assignment = true, assigned_at = now()
|
|
FROM servers s
|
|
LEFT JOIN zones z ON s.zone_id = z.id
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND z.name = :val
|
|
AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "uid": rule.user_id, "val": rule.rule_value})
|
|
|
|
# Ensuite propager par app_group (meme operateur pour recette+prod)
|
|
_auto_link_app_groups(db, campaign_id)
|
|
|
|
|
|
def _auto_link_app_groups(db, campaign_id):
|
|
"""Propage les intervenants entre recette et prod du meme app_group"""
|
|
assigned = db.execute(text("""
|
|
SELECT ps.intervenant_id, s.app_group
|
|
FROM patch_sessions ps
|
|
JOIN servers s ON ps.server_id = s.id
|
|
WHERE ps.campaign_id = :cid AND ps.intervenant_id IS NOT NULL AND s.app_group IS NOT NULL
|
|
GROUP BY ps.intervenant_id, s.app_group
|
|
"""), {"cid": campaign_id}).fetchall()
|
|
|
|
for a in assigned:
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :oid, assigned_at = now()
|
|
FROM servers s
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND s.app_group = :ag AND ps.intervenant_id IS NULL AND ps.status = 'pending'
|
|
"""), {"cid": campaign_id, "oid": a.intervenant_id, "ag": a.app_group})
|
|
|
|
|
|
def exclude_session(db, session_id, reason, detail, username):
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET
|
|
status = 'excluded', exclusion_reason = :reason,
|
|
exclusion_detail = :detail, excluded_by = :by, excluded_at = now()
|
|
WHERE id = :id
|
|
"""), {"id": session_id, "reason": reason, "detail": detail, "by": username})
|
|
_recalc_total(db, session_id)
|
|
db.commit()
|
|
|
|
|
|
def restore_session(db, session_id):
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET
|
|
status = 'pending', exclusion_reason = NULL,
|
|
exclusion_detail = NULL, excluded_by = NULL, excluded_at = NULL
|
|
WHERE id = :id
|
|
"""), {"id": session_id})
|
|
_recalc_total(db, session_id)
|
|
db.commit()
|
|
|
|
|
|
def _recalc_total(db, session_id):
|
|
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
|
|
{"id": session_id}).fetchone()
|
|
if row:
|
|
count = db.execute(text(
|
|
"SELECT COUNT(*) FROM patch_sessions WHERE campaign_id = :cid AND status NOT IN ('excluded','cancelled')"
|
|
), {"cid": row.campaign_id}).scalar()
|
|
db.execute(text("UPDATE campaigns SET total_servers = :c WHERE id = :cid"),
|
|
{"c": count, "cid": row.campaign_id})
|
|
|
|
|
|
def assign_operator(db, session_id, operator_id, forced=False):
|
|
"""Assigne un operateur a un serveur + auto-assigne le meme groupe applicatif"""
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET intervenant_id = :oid, assigned_at = now(),
|
|
forced_assignment = :forced
|
|
WHERE id = :id
|
|
"""), {"id": session_id, "oid": operator_id, "forced": forced})
|
|
|
|
# Auto-assigner les serveurs du meme app_group dans cette campagne
|
|
row = db.execute(text("""
|
|
SELECT ps.campaign_id, s.app_group FROM patch_sessions ps
|
|
JOIN servers s ON ps.server_id = s.id WHERE ps.id = :id
|
|
"""), {"id": session_id}).fetchone()
|
|
if row and row.app_group:
|
|
db.execute(text("""
|
|
UPDATE patch_sessions ps SET intervenant_id = :oid, assigned_at = now()
|
|
FROM servers s
|
|
WHERE ps.server_id = s.id AND ps.campaign_id = :cid
|
|
AND s.app_group = :ag AND ps.intervenant_id IS NULL
|
|
AND ps.status = 'pending'
|
|
"""), {"cid": row.campaign_id, "oid": operator_id, "ag": row.app_group})
|
|
|
|
db.commit()
|
|
|
|
|
|
def unassign_operator(db, session_id):
|
|
"""Desassigne un operateur"""
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET intervenant_id = NULL, assigned_at = NULL, forced_assignment = false
|
|
WHERE id = :id
|
|
"""), {"id": session_id})
|
|
db.commit()
|
|
|
|
|
|
def is_forced(db, session_id):
|
|
"""Verifie si l'assignation est forcee"""
|
|
row = db.execute(text("SELECT forced_assignment FROM patch_sessions WHERE id = :id"),
|
|
{"id": session_id}).fetchone()
|
|
return row.forced_assignment if row else False
|
|
|
|
|
|
def get_operator_count(db, campaign_id, operator_id):
|
|
"""Nombre de serveurs pris par un operateur dans cette campagne"""
|
|
return db.execute(text("""
|
|
SELECT COUNT(*) FROM patch_sessions
|
|
WHERE campaign_id = :cid AND intervenant_id = :oid AND status NOT IN ('excluded','cancelled')
|
|
"""), {"cid": campaign_id, "oid": operator_id}).scalar()
|
|
|
|
|
|
def get_operator_limit(db, campaign_id, operator_id):
|
|
"""Limite pour un operateur dans cette campagne (0=illimite)"""
|
|
row = db.execute(text("""
|
|
SELECT max_servers FROM campaign_operator_limits
|
|
WHERE campaign_id = :cid AND user_id = :uid
|
|
"""), {"cid": campaign_id, "uid": operator_id}).fetchone()
|
|
return row.max_servers if row else 0
|
|
|
|
|
|
def set_operator_limit(db, campaign_id, operator_id, max_servers, note=None):
|
|
"""Definit la limite pour un operateur dans cette campagne"""
|
|
db.execute(text("""
|
|
INSERT INTO campaign_operator_limits (campaign_id, user_id, max_servers, note)
|
|
VALUES (:cid, :uid, :max, :note)
|
|
ON CONFLICT (campaign_id, user_id) DO UPDATE SET max_servers = EXCLUDED.max_servers, note = EXCLUDED.note
|
|
"""), {"cid": campaign_id, "uid": operator_id, "max": max_servers, "note": note})
|
|
db.commit()
|
|
|
|
|
|
def get_campaign_operator_limits(db, campaign_id):
|
|
"""Retourne les limites de tous les operateurs pour une campagne"""
|
|
return db.execute(text("""
|
|
SELECT col.*, u.display_name
|
|
FROM campaign_operator_limits col
|
|
JOIN users u ON col.user_id = u.id
|
|
WHERE col.campaign_id = :cid ORDER BY u.display_name
|
|
"""), {"cid": campaign_id}).fetchall()
|
|
|
|
|
|
def update_session_schedule(db, session_id, date_prevue, heure_prevue):
|
|
"""Coordinateur ajuste la date/heure d'un serveur"""
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET date_prevue = :dp, heure_prevue = :hp
|
|
WHERE id = :id
|
|
"""), {"id": session_id, "dp": date_prevue or None, "hp": heure_prevue or None})
|
|
db.commit()
|
|
|
|
|
|
def validate_prereq(db, session_id, ssh, satellite, rollback, rollback_justif, username):
|
|
db.execute(text("""
|
|
UPDATE patch_sessions SET
|
|
prereq_ssh = :ssh, prereq_satellite = :sat,
|
|
rollback_method = :rb, rollback_justif = :rbj,
|
|
prereq_validated = CASE WHEN :ssh = 'ok' AND :sat = 'ok' AND :rb IS NOT NULL THEN true ELSE false END,
|
|
prereq_validated_by = :by, prereq_validated_at = now(), prereq_date = now()
|
|
WHERE id = :id
|
|
"""), {"id": session_id, "ssh": ssh, "sat": satellite,
|
|
"rb": rollback or None, "rbj": rollback_justif or None, "by": username})
|
|
db.commit()
|
|
|
|
|
|
def get_prereq_stats(db, campaign_id):
|
|
return db.execute(text("""
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE status = 'pending') as total_pending,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_validated = true) as prereq_ok,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_validated = false AND prereq_date IS NOT NULL) as prereq_ko,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_date IS NULL) as prereq_todo,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_ssh = 'ok') as ssh_ok,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_satellite = 'ok') as sat_ok,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND rollback_method IS NOT NULL) as rollback_ok,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND prereq_disk_ok = true) as disk_ok
|
|
FROM patch_sessions WHERE campaign_id = :cid
|
|
"""), {"cid": campaign_id}).fetchone()
|
|
|
|
|
|
def can_plan_campaign(db, campaign_id):
|
|
pending_not_validated = db.execute(text("""
|
|
SELECT COUNT(*) FROM patch_sessions
|
|
WHERE campaign_id = :cid AND status = 'pending' AND prereq_validated = false
|
|
"""), {"cid": campaign_id}).scalar()
|
|
return pending_not_validated == 0
|
|
|
|
|
|
def update_campaign_status(db, campaign_id, new_status):
|
|
db.execute(text("UPDATE campaigns SET status = :s WHERE id = :id"),
|
|
{"s": new_status, "id": campaign_id})
|
|
db.commit()
|