From 7bc96f937e405d8d98ece0c538e1cc66b4ea714b Mon Sep 17 00:00:00 2001 From: huesser Date: Tue, 16 Jun 2026 17:39:34 +0200 Subject: [PATCH] Working ad_lookup.py script --- .gitignore | 1 + docker-compose.yml | 3 +- logic/node-red-data/non_specified_groups.csv | 8 + logic/scripts/ad_lookup.py | 197 +++++++++++-------- 4 files changed, 126 insertions(+), 83 deletions(-) create mode 100644 logic/node-red-data/non_specified_groups.csv diff --git a/.gitignore b/.gitignore index d62132c..ff7c67c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ logic/scripts/.idea gaga* *backup .idea/ +*.log* diff --git a/docker-compose.yml b/docker-compose.yml index 3f821f1..ecabfc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,9 @@ services: - TZ=Europe/Zurich # Link to the newest archivegroup information file downloaded # from metabase.psi.ch - - GROUPINFO_JSON_LNK_NAME=archivegroup_information.json + #- GROUPINFO_JSON_LNK_NAME=archivegroup_information.json - GROUPINFO_JSON_LNK_NAME_WITH_PATH=/data/archivegroup_information.json + - NON_SPECIFIED_GROUPS=/data/non_specified_groups.csv # Beginning of name of the downloaded archivebroup information file - BEGIN_NAME_GROUPINFO_JSON_FILE=size_by_ownergroup_and_number_of_copies_ - ERROR_LOGFILE=error.log diff --git a/logic/node-red-data/non_specified_groups.csv b/logic/node-red-data/non_specified_groups.csv new file mode 100644 index 0000000..0924251 --- /dev/null +++ b/logic/node-red-data/non_specified_groups.csv @@ -0,0 +1,8 @@ +group,paymentunit,info +hipapie1,70000,wauters in description +unx-merlin_adm,7000,all members belong to the same group +unx-hipainterlock,8000,allmost all members belong to the same group +gac-x07da,6000,Raabe Joerg in the group description +EPFL,,NOT_IN_AD +Asmara,,NOT_IN_AD +P0001,,NOT_IN_AD diff --git a/logic/scripts/ad_lookup.py b/logic/scripts/ad_lookup.py index ee7f88f..a2dd680 100644 --- a/logic/scripts/ad_lookup.py +++ b/logic/scripts/ad_lookup.py @@ -1,23 +1,29 @@ """ -Dieses Skript liest eine JSON-Datei mit Gruppeninformationen ein und fragt für jede Gruppe -das zugehörige Department im Active Directory (AD) ab. Das Department ist in Form der entsprechenden -Zahl (z.B. 6000 für CPS) angegeben. +Dieses Skript reichert eine Liste von JSON-Objekten mit Department-Informationen aus dem +Active Directory (AD) an. -Die JSON-Datei ist eine Liste von Dictionaries, welche folgende Form haben: +Ablauf: +1. Eine JSON-Datei (definiert durch 'GROUPINFO_JSON_LNK_NAME_WITH_PATH') wird als + Datenbasis eingelesen. +2. Eine optionale, manuell gepflegte CSV-Datei (definiert durch 'NON_SPECIFIED_GROUPS') + wird geladen, um spezielle Gruppen direkt zuzuweisen und die AD-Abfrage zu überspringen. + Die CSV-Datei hat die Spalten: group,paymentunit,info. +3. Für jeden Eintrag aus der JSON-Datei wird das Department ermittelt: + a) Manuelle Zuweisung: Hat Priorität, falls die Gruppe in der CSV-Datei definiert ist. + b) a-Gruppen: Das Department wird direkt aus dem AD-Attribut der Gruppe gelesen. + c) p-Gruppen: Das Skript ermittelt in einem zweistufigen Prozess die zugehörige + Beamline-Gruppe und fragt von dieser das Department ab. Die Zuordnung + 'Beamline → Department' wird gecached, um die Anzahl der AD-Abfragen zu minimieren. +4. Das finale Ergebnis (die ursprüngliche Liste, ergänzt um 'department' und 'beamline') + wird als JSON-String auf stdout ausgegeben. Alle Log-Meldungen gehen auf stderr. -{"ownerGroup":"p18973","copies":"oneCopyBig","size":"6.85468397803E11","packedSize":"6.8563388416E11"} - -Es werden zwei Hauptfälle unterscheidet: -1. a-Gruppen (z.B. a-groupname): Diese Gruppen enthalten direkt das Attribut 'department'. -2. p-Gruppen (z.B. p12345): Diese Gruppen sind jeweils einer Beamline zugeordnet. Jede - Beamline besitzt ein 'department'-Attribut. Das Skript ermittelt zuerst die Beamline - und danach das Department. Die Zuordnung von Beamline zu Department wird gecached, - um AD-Anfragen zu minimieren, da es viele p-Gruppen aber nur wenige Beamlines gibt. - -Am Schluss wird eine Liste der eingelesenen Dictionaries ausgegeben, die allerdings durch -einen 'department' Eintrag ergänzt wurden. Bei den p-Gruppen wird zusätzlich noch ein 'beamline' -Eintrag hinzugefügt. +Resultat: +Jede Archivierungsgruppe (entspricht einem Dictionary in der Liste) besitzt am Schluss +einen zusaetzlichen Eintrag 'department', welcher den zugeordneten Bereich angibt. Falls +das Namensschema nicht stimmt, steht dort "unknown_scheme", falls im AD nichts gefunden +wird "not_found". """ +import csv import json import os import sys @@ -25,39 +31,66 @@ import sys from ldap3 import ALL, Connection, Server -# --- Konfiguration --- -AD_SERVER = 'd.psi.ch' -AD_SERVER = os.environ.get('AD_SERVER') -AD_USER = os.environ.get('AD_USER') -AD_PASSWORD = os.environ.get('AD_PASSWORD') -AD_SEARCH_BASE = 'DC=d,DC=psi,DC=ch' -AD_SEARCH_BASE = os.environ.get('AD_SEARCH_BASE') -FILE_PATH = os.environ.get('GROUPINFO_JSON_LNK_NAME_WITH_PATH') -# FILE_PATH = "/data/" + os.environ.get('GROUPINFO_JSON_LNK_NAME') - - def lookup_ad(): try: - # Datenbasis aus der via Umgebungsvariable definierten JSON-Datei laden - if FILE_PATH is None: - print("ERROR: Environment variable GROUPINFO_JSON_LNK_NAME_WITH_PATH is not set.", - file=sys.stderr, flush=True) + # --- 1. Konfiguration und Daten prüfen und einlesen --- + + # Umgebungsvariablen laden und prüfen + config = { + 'AD_SERVER': os.environ.get('AD_SERVER'), + 'AD_USER': os.environ.get('AD_USER'), + 'AD_PASSWORD': os.environ.get('AD_PASSWORD'), + 'AD_SEARCH_BASE': os.environ.get('AD_SEARCH_BASE'), + 'FILE_PATH': os.environ.get('GROUPINFO_JSON_LNK_NAME_WITH_PATH') + } + file_path_non_specified_groups = os.environ.get('NON_SPECIFIED_GROUPS') + + for key, value in config.items(): + if value is None: + print(f"ERROR: Environment variable {key} is not set.", file=sys.stderr, flush=True) + return + + # Sicheres Casten zu Strings für den Type-Checker + ad_server = str(config['AD_SERVER']) + ad_user = str(config['AD_USER']) + ad_password = str(config['AD_PASSWORD']) + ad_search_base = str(config['AD_SEARCH_BASE']) + file_path = str(config['FILE_PATH']) + + # JSON-Datei als Haupt-Datenquelle laden + if not os.path.exists(file_path): + print(f"ERROR: File {file_path} not found!", file=sys.stderr, flush=True) return - if not os.path.exists(FILE_PATH): - print(f"ERROR: File {FILE_PATH} not found!", file=sys.stderr, flush=True) - return - - with open(FILE_PATH, 'r') as f: + with open(file_path, 'r') as f: items = json.load(f) + print(f"INFO: {len(items)} entries loaded from JSON.", file=sys.stderr, flush=True) - print(f"INFO: {len(items)} entries loaded.", file=sys.stderr, flush=True) + # Optionale CSV-Datei mit manuellen Zuweisungen laden + manual_departments = {} + if file_path_non_specified_groups and os.path.exists(file_path_non_specified_groups): + with open(file_path_non_specified_groups, 'r', newline='') as f: + reader = csv.DictReader(f) + for row in reader: + group = row.get('group', '').strip() + department = row.get('paymentunit', '').strip() + info = row.get('info', '').strip() + + # Nur Gruppen hinzufügen, die nicht explizit als "nicht im AD" markiert sind + if group and department and info != 'NOT_IN_AD': + manual_departments[group] = department + + print(f"INFO: Loaded {len(manual_departments)} manual group definitions from CSV.", + file=sys.stderr, flush=True) + else: + print("INFO: No valid manual groups CSV file provided or found.", file=sys.stderr, flush=True) + + # --- 2. AD-Verarbeitung --- # AD-Verbindung via SSL (Port 636) initialisieren - server = Server(AD_SERVER, port=636, use_ssl=True, get_info=ALL, connect_timeout=10) - - with Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True) as conn: - # Cache für die Zuordnung von Beamline zu Department + server = Server(ad_server, port=636, use_ssl=True, get_info=ALL, connect_timeout=10) + with Connection(server, user=ad_user, password=ad_password, auto_bind=True) as conn: + # Cache für die Zuordnung von Beamline zu Department (Performance-Optimierung) beamline_department_cache = {} count = 0 @@ -65,96 +98,96 @@ def lookup_ad(): count += 1 group_name = item.get('ownerGroup', '') - # --- Fall A: direkte a-Gruppen --- + # Priorität 1: Manuelle Zuweisung aus CSV + if group_name in manual_departments: + item['department'] = manual_departments[group_name] + continue + + # Priorität 2: Direkte a-Gruppen if group_name.startswith('a-'): search_filter = f'(&(objectClass=group)(cn={group_name}))' conn.search( - search_base=AD_SEARCH_BASE, + search_base=ad_search_base, search_filter=search_filter, attributes=['department'] ) - - if conn.entries: - res = str(conn.entries[0].department) if 'department' in conn.entries[0] else "no-dept" - item['department'] = res + if conn.entries and 'department' in conn.entries[0]: + item['department'] = str(conn.entries[0].department) else: item['department'] = "not_found" - # --- Fall B: zweistufige p-Gruppen --- + # Priorität 3: Zweistufige p-Gruppen elif group_name.startswith('p') and group_name[1:2].isdigit(): - beamline = "not_found" # Initialisierung + beamline = "not_found" - # Stufe 1: 'memberOf' der p-Gruppe abfragen, um die zugehörige Beamline-Gruppe zu finden + # Stufe 1: Zugehörige Beamline-Gruppe via 'memberOf' ermitteln p_filter = f'(cn={group_name})' conn.search( - search_base=AD_SEARCH_BASE, + search_base=ad_search_base, search_filter=p_filter, attributes=['memberOf'] ) if conn.entries and 'memberOf' in conn.entries[0] and conn.entries[0].memberOf: - selected_dn = None - for dn in conn.entries[0].memberOf: - dn_str = str(dn) - if 'OU=Beamlines,OU=Experiment,OU=IT' in dn_str: - selected_dn = dn_str - break - + dn_generator = (str(dn) for dn in conn.entries[0].memberOf + if 'OU=Beamlines,OU=Experiment,OU=IT' in str(dn)) + selected_dn = next(dn_generator, None) + if selected_dn is None: selected_dn = str(conn.entries[0].memberOf[0]) try: beamline = selected_dn.split(',')[0].split('=')[1] except IndexError: - print(f"WARN: Format von DN '{selected_dn}' unerwartet.", file=sys.stderr, flush=True) + msg = f"WARN: Format von DN '{selected_dn}' unerwartet." + print(msg, file=sys.stderr, flush=True) beamline = "format_error" - + item['beamline'] = beamline - # Stufe 2: Department für die ermittelte Beamline abfragen (mit Caching) + # Stufe 2: Department für die Beamline ermitteln (mit Caching) if beamline in beamline_department_cache: item['department'] = beamline_department_cache[beamline] else: search_filter = f'(&(objectClass=group)(cn={beamline}))' conn.search( - search_base=AD_SEARCH_BASE, + search_base=ad_search_base, search_filter=search_filter, attributes=['department'] ) - - if conn.entries: - res = str(conn.entries[0].department) if 'department' in conn.entries[0] else "no-dept" - beamline_department_cache[beamline] = res # Department im Cache speichern - item['department'] = res + if conn.entries and 'department' in conn.entries[0]: + res = str(conn.entries[0].department) else: - beamline_department_cache[beamline] = "not_found" - item['department'] = "not_found" + res = "not_found" + beamline_department_cache[beamline] = res + item['department'] = res - # --- Fall C: Abweichende Schemata --- + # Fallback: Unbekannte Gruppenschemata else: - print(f"WARN: Unbekanntes Gruppen-Schema: {group_name}", file=sys.stderr, flush=True) + msg = f"WARNING: Unbekanntes Gruppen-Schema: {group_name}" + print(msg, file=sys.stderr, flush=True) item['department'] = "unknown_schema" - continue - - # Fortschritt alle 500 Einträge im stderr protokollieren + if count % 500 == 0: print(f"INFO: progress {count}/{len(items)}...", file=sys.stderr, flush=True) - # Kompletter JSON-Output (momentan auskommentiert): + # --- 3. Ausgabe --- + + # Finales Ergebnis als JSON auf stdout ausgeben print(json.dumps(items)) - # Alle Gruppen mit leerem Department-Feld ausgeben + # Zusätzliche Debug-Ausgabe auf stderr für Gruppen ohne valides Department for item in items: - if item.get('department') == "[]": + if item.get('department') in ("[]", "not_found", "no-dept"): + msg = f"WARNING: Kein Department für {item['ownerGroup']}" if 'beamline' in item: - print(f"LEER -> {item['ownerGroup']} (Beamline: {item['beamline']})", file=sys.stderr, flush=True) - else: - print(f"LEER -> {item['ownerGroup']} (hat keine Beamline)", file=sys.stderr, flush=True) - + msg += f" (Beamline: {item['beamline']})" + msg += " gefunden." + print(msg, file=sys.stderr, flush=True) except Exception as e: print(f"ERROR: {str(e)}", file=sys.stderr, flush=True) if __name__ == "__main__": - lookup_ad() \ No newline at end of file + lookup_ad()