375 lines
15 KiB
Python
375 lines
15 KiB
Python
# 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) |