# sysinfo.py (angepasst ohne cpuinfo und distro) # Version : 0.1.2 (aktualisiert) import os import sys import subprocess import json import psutil import platform import socket import logging import argparse from datetime import datetime # Nur versuchen zu importieren, wenn vorhanden try: import GPUtil gpu_available = True except ImportError: gpu_available = False # Versuche cpuinfo zu importieren try: import cpuinfo cpuinfo_available = True except ImportError: cpuinfo_available = False # Versuche distro zu importieren try: import distro distro_available = True except ImportError: distro_available = False # Globaler Debug-Status _debug_mode_global = False def set_debug_mode(mode: bool): global _debug_mode_global _debug_mode_global = mode # Farben für Debug-Logging class LogColors: INFO = "\033[94m" DEBUG = "\033[92m" WARNING = "\033[93m" ERROR = "\033[91m" RESET = "\033[0m" # Logging einrichten LOG_FILE = os.path.expanduser("~/hardware_info.log") if not logging.getLogger().handlers: # Verhindert doppelte Handler bei --reload logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) def log_message(level, message, debug_mode=False): # args.debug wird hier nicht direkt übergeben """Loggt Nachrichten mit Farben für die CLI, falls Debug aktiv ist.""" if level == "info": logging.info(message) if debug_mode: # Verwendet den lokalen debug_mode Parameter print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") elif level == "debug": logging.debug(message) if debug_mode: print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") elif level == "warning": logging.warning(message) if debug_mode: print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") elif level == "error": logging.error(message) if debug_mode: print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") def _get_last_login_linux(username, debug_mode=False): """ Ermittelt den letzten Login-Zeitpunkt für einen Benutzer unter Linux via 'lastlog'. Gibt einen formatierten String, "Nie", "N/A" oder "Unbekannt" zurück. """ try: # Erzwinge C-Locale für eine konsistente, nicht-lokalisierte Ausgabe env = os.environ.copy() env['LC_ALL'] = 'C' # Führe 'lastlog' für den spezifischen Benutzer aus result = subprocess.run( ['lastlog', '-u', username], capture_output=True, text=True, check=False, # Nicht abbrechen, wenn der Befehl fehlschlägt env=env ) # Wenn der Befehl fehlschlägt oder keine Ausgabe hat, abbrechen if result.returncode != 0 or not result.stdout: return "N/A" lines = result.stdout.strip().split('\n') # Erwarte Header + Benutzerzeile if len(lines) < 2: return "N/A" header = lines[0] user_line = lines[1] if "**Never logged in**" in user_line: return "Nie" # Finde die Startposition der "Latest"-Spalte im Header latest_pos = header.find("Latest") if latest_pos == -1: return "Unbekannt (Formatfehler)" # Extrahiere den Zeitstempel-String last_login_str = user_line[latest_pos:].strip() # Versuche, den String in ein sauberes Datumsformat zu parsen try: # Beispiel: "Tue May 21 10:00:15 +0200 2024" parts = last_login_str.split() # Baue einen String, den datetime.strptime parsen kann date_str_to_parse = f"{parts[5]} {parts[1]} {parts[2]} {parts[3]}" # "2024 May 21 10:00:15" dt_object = datetime.strptime(date_str_to_parse, '%Y %b %d %H:%M:%S') return dt_object.strftime('%Y-%m-%d %H:%M:%S') except (ValueError, IndexError): # Wenn das Parsen fehlschlägt, gib den Originalstring zurück return last_login_str except FileNotFoundError: log_message("warning", "Befehl 'lastlog' nicht gefunden. Kann letzte Logins nicht ermitteln.", debug_mode) return "N/A" except Exception as e: log_message("debug", f"Fehler beim Ermitteln des letzten Logins für {username}: {e}", debug_mode) return "N/A" def get_user_accounts(debug_mode=False): """ Ermittelt alle lokalen Benutzerkonten und ihren Anmeldestatus, inklusive Login-Zeit oder letztem Login. """ log_message("info", "Sammle Benutzerkonten-Informationen...", debug_mode) all_users = set() # Aktive Benutzer und deren Login-Zeiten erfassen active_users_info = {user.name: user.started for user in psutil.users()} active_users = set(active_users_info.keys()) system_type = platform.system() if system_type == "Linux": try: with open("/etc/passwd", "r") as f: for line in f: if not line.startswith(('#', '\n')): # Ignoriere Kommentare und leere Zeilen parts = line.split(':') if len(parts) > 0: username = parts[0] # Ignoriere Systembenutzer, die kein Login-Shell haben if not (username.startswith("_") or username.startswith("nobody") or username.startswith("systemd") or username.endswith("bus")): if "/bin/false" not in line and "/sbin/nologin" not in line: all_users.add(username) except Exception as e: log_message("warning", f"Konnte /etc/passwd nicht lesen: {str(e)}", debug_mode) # Fallback, falls /etc/passwd nicht zugreifbar ist all_users.update(active_users) elif system_type == "Windows": # Unter Windows ist es komplexer, alle lokalen Benutzer ohne 'net user' zu bekommen. # Für MVP nehmen wir hier die aktiven Benutzer + ggf. 'DefaultUser' als Platzhalter # Später könnte man subprocess.run(["net", "user"]) parsen. all_users.update(active_users) if "DefaultUser" not in all_users: # Ein häufiger Windows-Platzhalter all_users.add("DefaultUser") log_message("warning", "Detaillierte Benutzerkonten-Erfassung für Windows nicht vollständig implementiert.", debug_mode) elif system_type == "Darwin": # macOS # Unter macOS wäre 'dscl . -list /Users' der Weg, aber auch komplex zu parsen. all_users.update(active_users) log_message("warning", "Detaillierte Benutzerkonten-Erfassung für macOS nicht vollständig implementiert.", debug_mode) else: all_users.update(active_users) log_message("warning", f"Benutzerkonten-Erfassung für unbekanntes OS '{system_type}' nicht vollständig implementiert.", debug_mode) user_statuses = [] for user in sorted(list(all_users)): is_logged_in = user in active_users login_time = None last_login = None if is_logged_in: login_timestamp = active_users_info.get(user) if login_timestamp: login_time = datetime.fromtimestamp(login_timestamp).strftime('%Y-%m-%d %H:%M:%S') else: # Letzten Login für inaktive Benutzer ermitteln if system_type == "Linux": last_login = _get_last_login_linux(user, debug_mode) else: # Platzhalter für andere Betriebssysteme last_login = "N/A" user_statuses.append({ "name": user, "is_logged_in": is_logged_in, "login_time": login_time, "last_login": last_login }) log_message("info", f"Gefundene Benutzerkonten: {len(user_statuses)}", debug_mode) return user_statuses def get_hardware_info(): """Sammelt die relevanten Hardware- und Systeminformationen.""" hardware_info = {} # Betriebssystem log_message("info", "Sammle Betriebssystem-Daten...", _debug_mode_global) try: os_name = platform.system() os_version = platform.release() distribution = "Unbekannt" if distro_available: distribution = distro.name(pretty=True) elif os_name == "Linux": try: # Fallback für Linux-Distribution, falls 'distro' nicht verfügbar ist distribution = subprocess.check_output(["lsb_release", "-d", "-s"], text=True).strip() except (subprocess.CalledProcessError, FileNotFoundError): try: with open('/etc/redhat-release', 'r') as f: distribution = f.readline().strip() except FileNotFoundError: distribution = platform.platform() os_info = { "name": os_name, "version": os_version, "distribution": distribution } log_message("info", f"OS: {os_info['name']} - {os_info['distribution']} ({os_info['version']})", _debug_mode_global) except Exception as e: os_info = {"name": "Fehler", "version": "Fehler", "distribution": "Fehler"} log_message("warning", f"Konnte Betriebssystem-Daten nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["os"] = os_info # CPU log_message("info", "Sammle CPU-Daten...", _debug_mode_global) try: cpu_model = "Unbekannt" if cpuinfo_available: cpu_model = cpuinfo.get_cpu_info().get('brand_raw', 'Unbekannt') else: # Fallback für CPU-Modell ohne cpuinfo if platform.system() == "Windows": cpu_model = platform.processor() elif platform.system() == "Linux": try: with open('/proc/cpuinfo') as f: for line in f: if 'model name' in line: cpu_model = line.split(':')[1].strip() break except Exception: cpu_model = platform.processor() # Fallback, wenn /proc/cpuinfo nicht gelesen werden kann elif platform.system() == "Darwin": # macOS cpu_model = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip() cpu_info = { "model": cpu_model, "cores": psutil.cpu_count(logical=False), "threads": psutil.cpu_count(logical=True), "usage": psutil.cpu_percent(interval=0.1) # Kurzer Interval für schnelle Antwort } log_message("info", f"CPU: {cpu_info['model']} - {cpu_info['cores']} Cores, {cpu_info['threads']} Threads - {cpu_info['usage']}% Auslastung", _debug_mode_global) except Exception as e: cpu_info = {"model": "Fehler", "cores": 0, "threads": 0, "usage": 0.0} log_message("warning", f"Konnte CPU-Daten nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["cpu"] = cpu_info # RAM log_message("info", "Sammle RAM-Daten...", _debug_mode_global) try: ram = psutil.virtual_memory() ram_info = { "total": round(ram.total / (1024**3), 2), "used": round(ram.used / (1024**3), 2), "available": round(ram.available / (1024**3), 2), "percent": ram.percent } log_message("info", f"RAM: {ram_info['total']}GB gesamt, {ram_info['used']}GB genutzt, {ram_info['available']}GB verfügbar ({ram_info['percent']}%)", _debug_mode_global) except Exception as e: ram_info = {"total": 0.0, "used": 0.0, "available": 0.0, "percent": 0.0} log_message("warning", f"Konnte RAM-Daten nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["ram"] = ram_info # Festplatte log_message("info", "Sammle Festplattendaten...", _debug_mode_global) try: # Annahme: Root-Partition für Linux/macOS, C:\ für Windows path = '/' if platform.system() == "Windows": path = 'C:\\' disk_usage = psutil.disk_usage(path) disk_info = { "total": round(disk_usage.total / (1024**3), 2), "used": round(disk_usage.used / (1024**3), 2), "free": round(disk_usage.free / (1024**3), 2), "percent": disk_usage.percent } log_message("info", f"Disk ({path}): {disk_info['total']}GB gesamt, {disk_info['used']}GB genutzt, {disk_info['free']}GB frei ({disk_info['percent']}%)", _debug_mode_global) except Exception as e: disk_info = {"total": 0.0, "used": 0.0, "free": 0.0, "percent": 0.0} log_message("warning", f"Konnte Festplatten-Daten nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["disk"] = disk_info log_message("info", "Sammle Netzwerkinformationen (Basis)...", _debug_mode_global) try: network_info = { "hostname": socket.gethostname(), "ip_address": socket.gethostbyname(socket.gethostname()) # Gibt nur die erste gefundene IP zurück } log_message("info", f"Netzwerk (Basis): Hostname {network_info['hostname']}, IP {network_info['ip_address']}", _debug_mode_global) except Exception as e: network_info = {"hostname": "Fehler", "ip_address": "Fehler"} log_message("warning", f"Konnte Basis-Netzwerkinformationen nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["network_basic"] = network_info # Um Konflikt mit netinf zu vermeiden, umbenannt # Hinzufügen der detaillierten Benutzerinformationen hardware_info["user_accounts"] = get_user_accounts(_debug_mode_global) log_message("info", "Sammle dedizierte GPU-Daten...", _debug_mode_global) gpu_info = [] try: if gpu_available: gpus = GPUtil.getGPUs() for gpu in gpus: gpu_info.append({ "name": gpu.name, "memory_total_mb": gpu.memoryTotal, "memory_used_mb": gpu.memoryUsed, "memory_free_mb": gpu.memoryFree, "load_percent": round(gpu.load * 100, 1) }) log_message("info", f"Gefundene GPUs: {len(gpus)}", _debug_mode_global) else: log_message("warning", "GPUtil nicht installiert oder keine dedizierte GPU vorhanden.", _debug_mode_global) except Exception as e: log_message("warning", f"Konnte GPU-Daten nicht ermitteln: {str(e)}", _debug_mode_global) hardware_info["gpu"] = gpu_info log_message("info", "Hardware-Informationen erfolgreich erfasst.", _debug_mode_global) return hardware_info if __name__ == "__main__": parser = argparse.ArgumentParser(description="Ermittelt Hardware- und Systeminformationen.") parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") args = parser.parse_args() set_debug_mode(args.debug) hardware_info = get_hardware_info() if args.json: try: with open(args.json, "w") as json_file: json.dump(hardware_info, json_file, indent=4) log_message("info", f"Hardware-Infos in JSON gespeichert: {args.json}", args.debug) except Exception as e: log_message("error", f"Fehler beim Speichern der JSON-Datei: {str(e)}", args.debug)