patchcenter/tools/import_patch_history_xlsx.py
Admin MPCZ 38756fbfd6 Add import_patch_history_xlsx: lit sheets hebdo S02..S52, detecte lignes vertes
Pour chaque serveur avec fond vert col A: insere patch_history avec
date (col N), heure (col O si presente), note 'Semaine XX'. Fallback
sur le lundi de la semaine ISO si pas de date.
2026-04-14 21:30:43 +02:00

183 lines
6.3 KiB
Python

"""Import historique patching depuis Planning Patching 2026.xlsx.
Regle detection serveur patche : cellule hostname (col A) avec fond VERT.
Pour chaque sheet hebdo S02..S52 :
- Extrait num semaine depuis le nom de sheet ('S16' -> 16)
- Col A: hostname
- Col N (index 13): date (datetime ou string)
- Col O (index 14) si present: heure
- Ajoute patch_history(server_id, date_patch, status='ok', notes='Semaine X')
Usage:
python tools/import_patch_history_xlsx.py <xlsx> [--dry-run] [--sheets S16,S17]
"""
import os
import re
import argparse
from datetime import datetime, time, timedelta
from sqlalchemy import create_engine, text
try:
import openpyxl
except ImportError:
print("[ERR] pip install openpyxl")
raise
DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo"
# Codes couleur Excel "vert" (Office / standard)
GREEN_HEX_PREFIXES = ("00FF", "92D0", "C6EF", "A9D0", "B7E1", "55B5", "70AD",
"00B0", "00A0", "008", "00B5", "00C8")
def is_green(cell):
"""Detecte si la cellule a un fond vert."""
if cell.fill is None or cell.fill.fgColor is None:
return False
fc = cell.fill.fgColor
rgb = None
if fc.type == "rgb" and fc.rgb:
rgb = fc.rgb.upper()
elif fc.type == "theme":
# Themes Office : 9 = green1, 6 = accent2, etc. (approximatif)
if fc.theme in (9, 6, 4):
return True
if rgb and len(rgb) >= 6:
# AARRGGBB -> prendre le RRGGBB
rr = rgb[-6:-4]; gg = rgb[-4:-2]; bb = rgb[-2:]
try:
r, g, b = int(rr, 16), int(gg, 16), int(bb, 16)
# Vert dominant (G > R, G > B, G > 120)
return g > 120 and g > r + 30 and g > b + 30
except ValueError:
return False
return False
def parse_week_num(sheet_name):
m = re.search(r"[Ss](\d{1,2})", sheet_name)
return int(m.group(1)) if m else None
def week_to_date(year, week):
"""Retourne date du lundi de la semaine ISO."""
d = datetime.strptime(f"{year}-W{week:02d}-1", "%Y-W%W-%w")
return d.date()
def parse_hour(val):
"""Parse une cellule heure: '14:00', '14h', '14H30', datetime.time..."""
if val is None:
return None
if isinstance(val, time):
return val
if isinstance(val, datetime):
return val.time()
s = str(val).strip().lower().replace("h", ":")
m = re.match(r"(\d{1,2})(?::(\d{2}))?", s)
if not m:
return None
hh = int(m.group(1))
mm = int(m.group(2) or 0)
if 0 <= hh < 24 and 0 <= mm < 60:
return time(hh, mm)
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("xlsx_path")
parser.add_argument("--year", type=int, default=2026)
parser.add_argument("--sheets", default="", help="Liste de sheets filtrees, ex: S15,S16")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
engine = create_engine(DATABASE_URL)
conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT")
wb = openpyxl.load_workbook(args.xlsx_path, data_only=True)
target_sheets = set(s.strip().upper() for s in args.sheets.split(",") if s.strip()) if args.sheets else None
# Cache servers: hostname -> id
hosts = {}
for r in conn.execute(text("SELECT id, hostname FROM servers")).fetchall():
hosts[r.hostname.lower()] = r.id
stats = {"sheets": 0, "patched": 0, "inserted": 0, "no_server": 0, "skipped": 0}
for sname in wb.sheetnames:
wk = parse_week_num(sname)
if wk is None or not (1 <= wk <= 53):
continue
if target_sheets and sname.upper() not in target_sheets:
continue
ws = wb[sname]
stats["sheets"] += 1
default_date = week_to_date(args.year, wk)
for row_idx in range(2, ws.max_row + 1):
hn_cell = ws.cell(row=row_idx, column=1)
hn = hn_cell.value
if not hn:
continue
hn = str(hn).strip().split(".")[0].lower()
if not any(c.isalpha() for c in hn):
continue
if not is_green(hn_cell):
continue
stats["patched"] += 1
sid = hosts.get(hn)
if not sid:
stats["no_server"] += 1
continue
# Date col N (14), heure col O (15) si present
date_val = ws.cell(row=row_idx, column=14).value
hour_val = ws.cell(row=row_idx, column=15).value
dt_base = None
if isinstance(date_val, datetime):
dt_base = date_val
elif date_val:
try:
dt_base = datetime.strptime(str(date_val).split()[0], "%d/%m/%Y")
except Exception:
dt_base = None
if not dt_base:
dt_base = datetime.combine(default_date, time(0, 0))
hr = parse_hour(hour_val)
if hr:
dt_base = datetime.combine(dt_base.date(), hr)
note = f"Semaine {wk:02d}"
if args.dry_run:
print(f" DRY [{sname}] {hn:25s} -> {dt_base.isoformat()} ({note})")
else:
try:
# Evite doublons exacts (server+date)
existing = conn.execute(text(
"SELECT id FROM patch_history WHERE server_id=:sid AND date_patch=:dt"
), {"sid": sid, "dt": dt_base}).fetchone()
if existing:
stats["skipped"] += 1
continue
conn.execute(text("""
INSERT INTO patch_history (server_id, date_patch, status, notes)
VALUES (:sid, :dt, 'ok', :note)
"""), {"sid": sid, "dt": dt_base, "note": note})
stats["inserted"] += 1
except Exception as e:
print(f" [ERR] {hn}: {str(e)[:120]}")
stats["skipped"] += 1
conn.close()
print(f"\n[DONE] Sheets: {stats['sheets']} | Patches detectes: {stats['patched']} "
f"| Inserts: {stats['inserted']} | Sans serveur: {stats['no_server']} "
f"| Skip: {stats['skipped']}")
if __name__ == "__main__":
main()