diff --git a/ssacli_tool/ssacli_tool.py b/ssacli_tool/ssacli_tool.py new file mode 100644 index 0000000..3a51917 --- /dev/null +++ b/ssacli_tool/ssacli_tool.py @@ -0,0 +1,1040 @@ +#!/usr/bin/env python3 +""" +SSACLI Python Automation Tool +Automatisiert SSACLI-Befehle und parst die Ausgaben für einfache Verwaltung von HP Smart Array Controllern +""" + +import subprocess +import json +import yaml +import re +import argparse +from typing import Dict, List, Optional, Any +from dataclasses import dataclass, asdict +from datetime import datetime +from enum import Enum + +class OutputFormat(Enum): + """Verfügbare Ausgabeformate""" + TEXT = "text" + JSON = "json" + YAML = "yaml" + +@dataclass +class PhysicalDrive: + """Repräsentiert eine physische Festplatte""" + slot: str + status: str + size: str + interface: str + model: str + serial: str + temperature: Optional[str] = None + +@dataclass +class LogicalDrive: + """Repräsentiert ein logisches Laufwerk/Array""" + drive_id: str + size: str + raid_level: str + status: str + mount_point: Optional[str] = None + +@dataclass +class Array: + """Repräsentiert ein physisches Array""" + array_id: str + array_type: str + interface: str + size: str + unused_space: Optional[str] = None + status: str = "Unknown" + drive_count: int = 0 + drives: List[str] = None + + def __post_init__(self): + if self.drives is None: + self.drives = [] + +@dataclass +class UnassignedDrive: + """Repräsentiert ein nicht zugewiesenes Laufwerk""" + slot: str + size: str + interface: str + model: str + serial: str + +@dataclass +class Controller: + """Repräsentiert einen Smart Array Controller""" + slot: str + model: str + serial: str + cache_status: str + battery_status: Optional[str] = None + unassigned_space: Optional[str] = None + +class SSACLIManager: + """Hauptklasse für SSACLI-Operationen""" + + def __init__(self, ssacli_path: str = "ssacli"): + """ + Initialisiert den SSACLI Manager + + Args: + ssacli_path: Pfad zur SSACLI-Executable (Standard: "ssacli") + """ + self.ssacli_path = ssacli_path + self.controllers = [] + + def run_command(self, command: str) -> str: + """ + Führt einen SSACLI-Befehl aus und gibt die Ausgabe zurück + + Args: + command: SSACLI-Befehl ohne "ssacli" Präfix + + Returns: + Befehlsausgabe als String + """ + full_command = f"{self.ssacli_path} {command}" + try: + result = subprocess.run( + full_command, + shell=True, + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode != 0: + raise subprocess.CalledProcessError(result.returncode, full_command, result.stderr) + return result.stdout + except subprocess.TimeoutExpired: + raise Exception(f"Timeout beim Ausführen von: {full_command}") + except FileNotFoundError: + raise Exception(f"SSACLI nicht gefunden. Bitte Pfad überprüfen: {self.ssacli_path}") + + def discover_controllers(self) -> List[Controller]: + """ + Entdeckt alle verfügbaren Smart Array Controller + + Returns: + Liste der gefundenen Controller + """ + try: + output = self.run_command("ctrl all show") + controllers = [] + + # Parse Controller-Informationen + for line in output.split('\n'): + if 'Smart Array' in line and 'Slot' in line: + # Beispiel: "Smart Array P420i in Slot 0 (Embedded)" + slot_match = re.search(r'Slot (\d+)', line) + model_match = re.search(r'Smart Array ([^)]+)', line) + + if slot_match and model_match: + slot = slot_match.group(1) + model = model_match.group(1).strip() + + # Detaillierte Controller-Info abrufen + detail_output = self.run_command(f"ctrl slot={slot} show") + serial = self._extract_field(detail_output, "Serial Number") + cache_status = self._extract_field(detail_output, "Cache Status") + battery_status = self._extract_field(detail_output, "Battery/Capacitor Status") + + controller = Controller( + slot=slot, + model=model, + serial=serial or "N/A", + cache_status=cache_status or "Unknown", + battery_status=battery_status, + unassigned_space=None # Wird später berechnet + ) + controllers.append(controller) + + self.controllers = controllers + return controllers + + except Exception as e: + print(f"Fehler beim Entdecken der Controller: {e}") + return [] + + def get_physical_drives(self, controller_slot: str) -> List[PhysicalDrive]: + """ + Ruft alle physischen Laufwerke für einen Controller ab + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Liste der physischen Laufwerke + """ + try: + output = self.run_command(f"ctrl slot={controller_slot} pd all show") + drives = [] + + # Parse physische Laufwerke + for line in output.split('\n'): + if 'physicaldrive' in line.lower(): + # Beispiel: "physicaldrive 1I:1:1 (port 1I:box 1:bay 1, SAS, 300 GB, OK)" + match = re.search(r'physicaldrive (\S+).*?\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)', line) + if match: + slot = match.group(1) + interface = match.group(3).strip() + size = match.group(4).strip() + status = match.group(5).strip() + + # Detaillierte Laufwerk-Info + detail_output = self.run_command(f"ctrl slot={controller_slot} pd {slot} show") + model = self._extract_field(detail_output, "Model") + serial = self._extract_field(detail_output, "Serial Number") + temp = self._extract_field(detail_output, "Current Temperature") + + drive = PhysicalDrive( + slot=slot, + status=status, + size=size, + interface=interface, + model=model or "Unknown", + serial=serial or "N/A", + temperature=temp + ) + drives.append(drive) + + return drives + + except Exception as e: + print(f"Fehler beim Abrufen der physischen Laufwerke: {e}") + return [] + + def get_unassigned_drives(self, controller_slot: str): + try: + output = self.run_command(f"ctrl slot={controller_slot} pd allunassigned show") + drives = [] + + # Parse physische Laufwerke + for line in output.split('\n'): + if 'physicaldrive' in line.lower(): + # Beispiel: "physicaldrive 1I:1:1 (port 1I:box 1:bay 1, SAS, 300 GB, OK)" + match = re.search(r'physicaldrive (\S+).*?\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)', line) + if match: + slot = match.group(1) + interface = match.group(3).strip() + size = match.group(4).strip() + status = match.group(5).strip() + + # Detaillierte Laufwerk-Info + detail_output = self.run_command(f"ctrl slot={controller_slot} pd {slot} show") + model = self._extract_field(detail_output, "Model") + serial = self._extract_field(detail_output, "Serial Number") + temp = self._extract_field(detail_output, "Current Temperature") + + drive = PhysicalDrive( + slot=slot, + status=status, + size=size, + interface=interface, + model=model or "Unknown", + serial=serial or "N/A", + temperature=temp + ) + drives.append(drive) + + return drives + + except Exception as e: + print(f"Fehler beim Abrufen der unassigned Laufwerke: {e}") + return [] + + def get_logical_drives(self, controller_slot: str) -> List[LogicalDrive]: + """ + Ruft alle logischen Laufwerke für einen Controller ab + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Liste der logischen Laufwerke + """ + try: + output = self.run_command(f"ctrl slot={controller_slot} ld all show") + drives = [] + + # Parse logische Laufwerke + for line in output.split('\n'): + if 'logicaldrive' in line.lower(): + # Beispiel: "logicaldrive 1 (279.4 GB, RAID 1, OK)" + match = re.search(r'logicaldrive (\d+) \(([^,]+),\s*([^,]+),\s*([^)]+)', line) + if match: + drive_id = match.group(1) + size = match.group(2).strip() + raid_level = match.group(3).strip() + status = match.group(4).strip() + + drive = LogicalDrive( + drive_id=drive_id, + size=size, + raid_level=raid_level, + status=status + ) + drives.append(drive) + + return drives + + except Exception as e: + print(f"Fehler beim Abrufen der logischen Laufwerke: {e}") + return [] + + def get_arrays(self, controller_slot: str) -> List[Array]: + """ + Ruft alle Arrays für einen Controller ab + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Liste der Arrays mit ungenutztem Speicher + """ + try: + output = self.run_command(f"ctrl slot={controller_slot} array all show") + arrays = [] + + # Parse Arrays + current_array = None + for line in output.split('\n'): + line = line.strip() + + # Array Header erkennen + if line.startswith('array ') and '(' in line: + # Beispiel: "array A (SAS, Unused Space: 558.9 GB)" + match = re.search(r'array ([A-Z]+) \(([^,]+)(?:,\s*Unused Space:\s*([^)]+))?\)', line) + if match: + array_id = match.group(1) + interface = match.group(2).strip() + unused_space = match.group(3) if match.group(3) else "0 GB" + + # Detaillierte Array-Info abrufen + try: + detail_output = self.run_command(f"ctrl slot={controller_slot} array {array_id} show") + + # Array-Typ und Status extrahieren + array_type = self._extract_field(detail_output, "Array Type") or "Unknown" + status = self._extract_field(detail_output, "Status") or "Unknown" + + # Array-Größe extrahieren + size_match = re.search(r'Array Size:\s*([^,\n]+)', detail_output) + size = size_match.group(1).strip() if size_match else "Unknown" + + # Physische Laufwerke in diesem Array zählen + drive_count = len(re.findall(r'physicaldrive \d+[A-Z]:\d+:\d+', detail_output)) + + # Laufwerk-Slots extrahieren + drive_slots = re.findall(r'physicaldrive (\d+[A-Z]:\d+:\d+)', detail_output) + + current_array = Array( + array_id=array_id, + array_type=array_type, + interface=interface, + size=size, + unused_space=unused_space, + status=status, + drive_count=drive_count, + drives=drive_slots + ) + arrays.append(current_array) + + except Exception as detail_error: + print(f"Warnung: Konnte Details für Array {array_id} nicht abrufen: {detail_error}") + # Fallback mit verfügbaren Informationen + current_array = Array( + array_id=array_id, + array_type="Unknown", + interface=interface, + size="Unknown", + unused_space=unused_space, + status="Unknown", + drive_count=0, + drives=[] + ) + arrays.append(current_array) + + return arrays + + except Exception as e: + print(f"Fehler beim Abrufen der Arrays: {e}") + return [] + + def calculate_array_unused_space(self, controller_slot: str) -> Dict[str, Any]: + """ + Berechnet ungenutzten Speicherplatz in bestehenden Arrays + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Dictionary mit ungenutztem Array-Speicher und möglichen LD-Konfigurationen + """ + try: + arrays = self.get_arrays(controller_slot) + + total_unused_gb = 0.0 + array_details = [] + possible_logical_drives = {} + + for array in arrays: + unused_space_gb = 0.0 + + # Parse unused space + if array.unused_space and array.unused_space != "0 GB": + unused_match = re.search(r'(\d+(?:\.\d+)?)\s*(GB|TB)', array.unused_space, re.IGNORECASE) + if unused_match: + unused_value = float(unused_match.group(1)) + unit = unused_match.group(2).upper() + if unit == "TB": + unused_value *= 1024 + unused_space_gb = unused_value + total_unused_gb += unused_space_gb + + array_info = { + "array_id": array.array_id, + "array_type": array.array_type, + "interface": array.interface, + "total_size": array.size, + "unused_space": array.unused_space, + "unused_space_gb": unused_space_gb, + "status": array.status, + "drive_count": array.drive_count, + "drives": array.drives + } + array_details.append(array_info) + + # Mögliche logische Laufwerke für dieses Array + if unused_space_gb > 0: + array_key = f"Array {array.array_id} ({array.interface})" + possible_logical_drives[array_key] = { + "available_space_gb": unused_space_gb, + "available_space": f"{unused_space_gb:.1f} GB", + "suggested_sizes": [] + } + + # Vorschläge für logische Laufwerk-Größen + if unused_space_gb >= 1000: # 1TB+ + possible_logical_drives[array_key]["suggested_sizes"].extend([ + f"{unused_space_gb:.0f} GB (kompletter verfügbarer Platz)", + "500 GB", + "100 GB" + ]) + elif unused_space_gb >= 100: # 100GB+ + possible_logical_drives[array_key]["suggested_sizes"].extend([ + f"{unused_space_gb:.0f} GB (kompletter verfügbarer Platz)", + "50 GB", + "20 GB" + ]) + else: # Weniger als 100GB + possible_logical_drives[array_key]["suggested_sizes"].append( + f"{unused_space_gb:.1f} GB (kompletter verfügbarer Platz)" + ) + + return { + "total_unused_space_gb": total_unused_gb, + "total_unused_space": f"{total_unused_gb:.1f} GB", + "arrays_with_unused_space": len([a for a in array_details if a["unused_space_gb"] > 0]), + "array_details": array_details, + "possible_logical_drives": possible_logical_drives + } + + except Exception as e: + print(f"Fehler beim Berechnen des ungenutzten Array-Speichers: {e}") + return { + "total_unused_space_gb": 0.0, + "total_unused_space": "0 GB", + "arrays_with_unused_space": 0, + "array_details": [], + "possible_logical_drives": {} + } + """ + Ruft alle nicht zugewiesenen physischen Laufwerke ab + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Liste der nicht zugewiesenen Laufwerke + """ + try: + output = self.run_command(f"ctrl slot={controller_slot} pd all show") + unassigned_drives = [] + + # Parse nicht zugewiesene Laufwerke + for line in output.split('\n'): + if 'physicaldrive' in line.lower() and ('unassigned' in line.lower() or 'ready' in line.lower()): + # Beispiel: "physicaldrive 1I:1:2 (port 1I:box 1:bay 2, SAS, 300 GB, OK, unassigned)" + match = re.search(r'physicaldrive (\S+).*?\(([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,)]+)', line) + if match: + slot = match.group(1) + interface = match.group(3).strip() + size = match.group(4).strip() + + # Detaillierte Info für nicht zugewiesene Laufwerke + detail_output = self.run_command(f"ctrl slot={controller_slot} pd {slot} show") + model = self._extract_field(detail_output, "Model") + serial = self._extract_field(detail_output, "Serial Number") + + # Prüfen ob wirklich nicht zugewiesen + if "unassigned" in detail_output.lower() or "ready" in detail_output.lower(): + drive = UnassignedDrive( + slot=slot, + size=size, + interface=interface, + model=model or "Unknown", + serial=serial or "N/A" + ) + unassigned_drives.append(drive) + + return unassigned_drives + + except Exception as e: + print(f"Fehler beim Abrufen der nicht zugewiesenen Laufwerke: {e}") + return [] + + def calculate_available_space(self, controller_slot: str) -> Dict[str, Any]: + """ + Berechnet verfügbaren Speicherplatz für neue logische Laufwerke + + Args: + controller_slot: Controller Slot-Nummer + + Returns: + Dictionary mit verfügbarem Speicher und möglichen RAID-Konfigurationen + """ + try: + unassigned_drives = self.get_unassigned_drives(controller_slot) + + if not unassigned_drives: + return { + "total_unassigned_drives": 0, + "available_space": "0 GB", + "possible_raids": {}, + "drives": [] + } + + # Gruppiere Laufwerke nach Größe und Interface + drive_groups = {} + total_space_gb = 0 + + for drive in unassigned_drives: + # Extrahiere Größe in GB + size_match = re.search(r'(\d+(?:\.\d+)?)\s*(GB|TB)', drive.size, re.IGNORECASE) + if size_match: + size_value = float(size_match.group(1)) + unit = size_match.group(2).upper() + if unit == "TB": + size_value *= 1024 + + key = f"{drive.size}_{drive.interface}" + if key not in drive_groups: + drive_groups[key] = { + "size": drive.size, + "size_gb": size_value, + "interface": drive.interface, + "count": 0, + "drives": [] + } + + drive_groups[key]["count"] += 1 + drive_groups[key]["drives"].append(drive.slot) + total_space_gb += size_value + + # Berechne mögliche RAID-Konfigurationen + possible_raids = {} + + for group_key, group in drive_groups.items(): + count = group["count"] + size_gb = group["size_gb"] + + group_raids = {} + + # RAID 0: Mindestens 2 Laufwerke + if count >= 2: + raid0_capacity = count * size_gb + group_raids["RAID 0"] = { + "drives_needed": count, + "capacity_gb": raid0_capacity, + "description": f"Alle {count} Laufwerke, {raid0_capacity:.1f} GB nutzbar" + } + + # RAID 1: Genau 2 Laufwerke + if count >= 2: + pairs = count // 2 + raid1_capacity = pairs * size_gb + group_raids["RAID 1"] = { + "drives_needed": pairs * 2, + "capacity_gb": raid1_capacity, + "description": f"{pairs} Paar(e), {raid1_capacity:.1f} GB nutzbar" + } + + # RAID 5: Mindestens 3 Laufwerke + if count >= 3: + raid5_capacity = (count - 1) * size_gb + group_raids["RAID 5"] = { + "drives_needed": count, + "capacity_gb": raid5_capacity, + "description": f"{count} Laufwerke, {raid5_capacity:.1f} GB nutzbar" + } + + # RAID 6: Mindestens 4 Laufwerke + if count >= 4: + raid6_capacity = (count - 2) * size_gb + group_raids["RAID 6"] = { + "drives_needed": count, + "capacity_gb": raid6_capacity, + "description": f"{count} Laufwerke, {raid6_capacity:.1f} GB nutzbar" + } + + # RAID 10: Mindestens 4 Laufwerke, gerade Anzahl + if count >= 4 and count % 2 == 0: + raid10_capacity = (count // 2) * size_gb + group_raids["RAID 10"] = { + "drives_needed": count, + "capacity_gb": raid10_capacity, + "description": f"{count} Laufwerke, {raid10_capacity:.1f} GB nutzbar" + } + + if group_raids: + possible_raids[f"{group['size']} {group['interface']} ({count}x)"] = group_raids + + return { + "total_unassigned_drives": len(unassigned_drives), + "available_space": f"{total_space_gb:.1f} GB", + "possible_raids": possible_raids, + "drives": [ + { + "slot": drive.slot, + "size": drive.size, + "interface": drive.interface, + "model": drive.model, + "serial": drive.serial + } + for drive in unassigned_drives + ], + "drive_groups": drive_groups + } + + except Exception as e: + print(f"Fehler beim Berechnen des verfügbaren Speichers: {e}") + return { + "total_unassigned_drives": 0, + "available_space": "0 GB", + "possible_raids": {}, + "drives": [] + } + """Hilfsfunktion zum Extrahieren von Feldern aus SSACLI-Ausgabe""" + pattern = f"{field_name}\\s*:\\s*(.+)" + match = re.search(pattern, output, re.IGNORECASE) + return match.group(1).strip() if match else None + + def create_raid_array(self, controller_slot: str, raid_level: str, drives: List[str]) -> bool: + """ + Erstellt ein neues RAID-Array + + Args: + controller_slot: Controller Slot-Nummer + raid_level: RAID-Level (0, 1, 5, 6, 10) + drives: Liste der physischen Laufwerk-Slots + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + drives_str = ",".join(drives) + command = f"ctrl slot={controller_slot} create type=ld drives={drives_str} raid={raid_level}" + output = self.run_command(command) + return "Success" in output or "OK" in output + except Exception as e: + print(f"Fehler beim Erstellen des RAID-Arrays: {e}") + return False + + def generate_report(self) -> Dict[str, Any]: + """ + Generiert einen umfassenden Bericht über alle Controller und Laufwerke + + Returns: + Dictionary mit allen System-Informationen + """ + report = { + "timestamp": datetime.now().isoformat(), + "controllers": [] + } + + controllers = self.discover_controllers() + + for controller in controllers: + controller_data = { + "controller": { + "slot": controller.slot, + "model": controller.model, + "serial": controller.serial, + "cache_status": controller.cache_status, + "battery_status": controller.battery_status + }, + "physical_drives": [], + "logical_drives": [] + } + + # Physische Laufwerke + physical_drives = self.get_physical_drives(controller.slot) + for pd in physical_drives: + controller_data["physical_drives"].append({ + "slot": pd.slot, + "status": pd.status, + "size": pd.size, + "interface": pd.interface, + "model": pd.model, + "serial": pd.serial, + "temperature": pd.temperature + }) + + # Logische Laufwerke + logical_drives = self.get_logical_drives(controller.slot) + for ld in logical_drives: + controller_data["logical_drives"].append({ + "drive_id": ld.drive_id, + "size": ld.size, + "raid_level": ld.raid_level, + "status": ld.status + }) + + # Verfügbarer Speicher und mögliche RAIDs + available_space_info = self.calculate_available_space(controller.slot) + controller_data["available_space"] = available_space_info + + # Ungenutzter Array-Speicher + array_unused_info = self.calculate_array_unused_space(controller.slot) + controller_data["array_unused_space"] = array_unused_info + + report["controllers"].append(controller_data) + + return report + + def print_summary(self, output_format: OutputFormat = OutputFormat.TEXT): + """ + Gibt eine Zusammenfassung aller Systeminformationen aus + + Args: + output_format: Gewünschtes Ausgabeformat (TEXT, JSON, YAML) + """ + if output_format == OutputFormat.TEXT: + self._print_text_summary() + elif output_format == OutputFormat.JSON: + self._print_json_summary() + elif output_format == OutputFormat.YAML: + self._print_yaml_summary() + else: + raise ValueError(f"Unbekanntes Ausgabeformat: {output_format}") + + def _extract_field(self, detail_output, field): + """ + serial = self._extract_field(detail_output, "Serial Number") + + Serial Number: PZXND0BRHH68OI + """ + try: + for line in iter(detail_output.splitlines()): + line = line.strip() + line = line.split(':') + if line[0] == field: + return(line[1]) + except: + return('') + + def _print_text_summary(self): + """Druckt eine übersichtliche Text-Zusammenfassung für die Kommandozeile""" + print("=" * 80) + print("HP Smart Array Controller Zusammenfassung") + print("=" * 80) + + controllers = self.discover_controllers() + + if not controllers: + print("Keine Smart Array Controller gefunden!") + return + + for controller in controllers: + print(f"\n📊 Controller Slot {controller.slot}: {controller.model}") + print(f" Serial: {controller.serial}") + print(f" Cache Status: {controller.cache_status}") + if controller.battery_status: + print(f" Battery Status: {controller.battery_status}") + + # Physische Laufwerke + physical_drives = self.get_physical_drives(controller.slot) + print(f"\n💾 Physische Laufwerke ({len(physical_drives)}):") + for pd in physical_drives: + status_emoji = "✅" if pd.status == "OK" else "❌" + temp_info = f" ({pd.temperature})" if pd.temperature else "" + print(f" {status_emoji} {pd.slot}: {pd.size} {pd.interface} - {pd.status}{temp_info}") + + # Logische Laufwerke + logical_drives = self.get_logical_drives(controller.slot) + print(f"\n🔗 Logische Laufwerke ({len(logical_drives)}):") + for ld in logical_drives: + status_emoji = "✅" if ld.status == "OK" else "❌" + print(f" {status_emoji} LD {ld.drive_id}: {ld.size} {ld.raid_level} - {ld.status}") + + # Verfügbarer Speicher + available_info = self.calculate_available_space(controller.slot) + unassigned_count = available_info["total_unassigned_drives"] + + if unassigned_count > 0: + print(f"\n💾 Nicht zugewiesener Speicher:") + print(f" 📦 {unassigned_count} Laufwerk(e) verfügbar ({available_info['available_space']})") + + # Zeige verfügbare Laufwerke + for drive in available_info["drives"]: + print(f" 🔸 {drive['slot']}: {drive['size']} {drive['interface']} - {drive['model']}") + + # Zeige mögliche RAID-Konfigurationen + if available_info["possible_raids"]: + print(f"\n🛠️ Mögliche RAID-Konfigurationen:") + for group_name, raids in available_info["possible_raids"].items(): + print(f" 📋 {group_name}:") + for raid_type, raid_info in raids.items(): + print(f" • {raid_type}: {raid_info['description']}") + + # Ungenutzter Array-Speicher + array_unused_info = self.calculate_array_unused_space(controller.slot) + total_unused = array_unused_info["total_unused_space_gb"] + arrays_with_unused = array_unused_info["arrays_with_unused_space"] + + if total_unused > 0: + print(f"\n🔧 Ungenutzter Array-Speicher:") + print(f" 📊 {arrays_with_unused} Array(s) mit ungenutztem Platz ({array_unused_info['total_unused_space']})") + + for array_info in array_unused_info["array_details"]: + if array_info["unused_space_gb"] > 0: + print(f" 🔸 Array {array_info['array_id']}: {array_info['unused_space']} verfügbar") + print(f" └─ {array_info['interface']}, {array_info['drive_count']} Laufwerke, Status: {array_info['status']}") + + # Mögliche logische Laufwerke + if array_unused_info["possible_logical_drives"]: + print(f"\n💿 Mögliche neue logische Laufwerke:") + for array_name, ld_info in array_unused_info["possible_logical_drives"].items(): + print(f" 📋 {array_name} ({ld_info['available_space']} verfügbar):") + for size in ld_info["suggested_sizes"]: + print(f" • {size}") + + if unassigned_count == 0 and total_unused == 0: + print(f"\n💾 Speicher-Status: Alle verfügbaren Laufwerke sind vollständig genutzt ✅") + + def _print_json_summary(self): + """Gibt eine JSON-formatierte Zusammenfassung aus""" + report = self.generate_report() + print(json.dumps(report, indent=2, ensure_ascii=False)) + + def _print_yaml_summary(self): + """Gibt eine YAML-formatierte Zusammenfassung aus""" + report = self.generate_report() + print(yaml.dump(report, default_flow_style=False, allow_unicode=True, sort_keys=False)) + + def export_report(self, filename: str, output_format: OutputFormat): + """ + Exportiert einen detaillierten Bericht in eine Datei + + Args: + filename: Zieldateiname + output_format: Gewünschtes Ausgabeformat + """ + report = self.generate_report() + + try: + with open(filename, 'w', encoding='utf-8') as f: + if output_format == OutputFormat.JSON: + json.dump(report, f, indent=2, ensure_ascii=False) + elif output_format == OutputFormat.YAML: + yaml.dump(report, f, default_flow_style=False, allow_unicode=True, sort_keys=False) + elif output_format == OutputFormat.TEXT: + # Text-Export als formatierte Ausgabe + f.write("=" * 80 + "\n") + f.write("HP Smart Array Controller Bericht\n") + f.write(f"Generiert: {report['timestamp']}\n") + f.write("=" * 80 + "\n\n") + + for controller_data in report["controllers"]: + controller = controller_data["controller"] + f.write(f"Controller Slot {controller['slot']}: {controller['model']}\n") + f.write(f" Serial: {controller['serial']}\n") + f.write(f" Cache Status: {controller['cache_status']}\n") + if controller.get('battery_status'): + f.write(f" Battery Status: {controller['battery_status']}\n") + + f.write(f"\n Physische Laufwerke ({len(controller_data['physical_drives'])}):\n") + for pd in controller_data["physical_drives"]: + f.write(f" {pd['slot']}: {pd['size']} {pd['interface']} - {pd['status']}\n") + + f.write(f"\n Logische Laufwerke ({len(controller_data['logical_drives'])}):\n") + for ld in controller_data["logical_drives"]: + f.write(f" LD {ld['drive_id']}: {ld['size']} {ld['raid_level']} - {ld['status']}\n") + + # Verfügbarer Speicher + available = controller_data.get("available_space", {}) + if available.get("total_unassigned_drives", 0) > 0: + f.write(f"\n Nicht zugewiesener Speicher: {available['available_space']}\n") + for drive in available.get("drives", []): + f.write(f" {drive['slot']}: {drive['size']} {drive['interface']}\n") + + # Array-Speicher + array_unused = controller_data.get("array_unused_space", {}) + if array_unused.get("total_unused_space_gb", 0) > 0: + f.write(f"\n Ungenutzter Array-Speicher: {array_unused['total_unused_space']}\n") + for array_info in array_unused.get("array_details", []): + if array_info.get("unused_space_gb", 0) > 0: + f.write(f" Array {array_info['array_id']}: {array_info['unused_space']}\n") + + f.write("\n" + "-" * 60 + "\n") + + print(f"✅ Bericht exportiert: {filename}") + + except Exception as e: + print(f"❌ Fehler beim Exportieren: {e}") + + +def main(): + """Hauptfunktion mit Kommandozeilen-Interface""" + parser = argparse.ArgumentParser( + description="SSACLI Python Automation Tool - Verwaltung von HP Smart Array Controllern", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Beispiele: + python3 ssacli_tool.py # Standard Text-Ausgabe + python3 ssacli_tool.py --format json # JSON-Ausgabe + python3 ssacli_tool.py --format yaml # YAML-Ausgabe + python3 ssacli_tool.py --export report.json # JSON-Export in Datei + python3 ssacli_tool.py --export report.yaml --format yaml # YAML-Export + python3 ssacli_tool.py --ssacli-path /usr/sbin/ssacli # Alternativer SSACLI-Pfad + +Hinweis: Das Script muss als Administrator/Root ausgeführt werden. + """ + ) + + parser.add_argument( + '--format', '-f', + type=str, + choices=['text', 'json', 'yaml'], + default='text', + help='Ausgabeformat (Standard: text)' + ) + + parser.add_argument( + '--export', '-e', + type=str, + help='Exportiere Bericht in angegebene Datei' + ) + + parser.add_argument( + '--ssacli-path', + type=str, + default='ssacli', + help='Pfad zur SSACLI-Executable (Standard: ssacli)' + ) + + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Unterdrücke Statusmeldungen (nur Ausgabe/Export)' + ) + + parser.add_argument( + '--version', '-v', + action='version', + version='SSACLI Python Tool v2.0' + ) + + args = parser.parse_args() + + # Ausgabeformat bestimmen + try: + output_format = OutputFormat(args.format) + except ValueError: + print(f"❌ Unbekanntes Format: {args.format}") + return 1 + + if not args.quiet: + print("🚀 SSACLI Python Automation Tool v2.0") + if args.format != 'text': + print(f"📋 Ausgabeformat: {args.format.upper()}") + print("-" * 50) + + # SSACLI Manager initialisieren + ssacli = SSACLIManager(args.ssacli_path) + + try: + # Export in Datei + if args.export: + if not args.quiet: + print(f"📤 Exportiere Bericht nach {args.export}...") + + ssacli.export_report(args.export, output_format) + + if not args.quiet: + print("✅ Export erfolgreich abgeschlossen") + + # Ausgabe auf Konsole + else: + if not args.quiet and args.format == 'text': + print() + + ssacli.print_summary(output_format) + + return 0 + + except KeyboardInterrupt: + if not args.quiet: + print("\n⚠️ Abgebrochen durch Benutzer") + return 130 + + except Exception as e: + print(f"❌ Fehler: {e}") + if not args.quiet: + print("\nHinweise:") + print("- Stellen Sie sicher, dass SSACLI installiert ist") + print("- Script als Administrator/Root ausführen") + print("- Smart Array Controller müssen vorhanden sein") + print(f"- SSACLI-Pfad prüfen: {args.ssacli_path}") + return 1 + + +# Legacy-Funktion für Rückwärtskompatibilität +def legacy_main(): + """Legacy Hauptfunktion für alte Verwendung""" + print("🚀 SSACLI Python Automation Tool") + print("-" * 40) + + # SSACLI Manager initialisieren + ssacli = SSACLIManager() + + try: + # Übersicht anzeigen + ssacli.print_summary() + + # Detaillierten JSON-Report generieren + print("\n📋 Generiere detaillierten Report...") + report = ssacli.generate_report() + + # Report in Datei speichern + report_filename = f"ssacli_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + with open(report_filename, 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2, ensure_ascii=False) + + print(f"✅ Report gespeichert: {report_filename}") + + except Exception as e: + print(f"❌ Fehler: {e}") + print("\nHinweis: Stellen Sie sicher, dass:") + print("- SSACLI installiert ist") + print("- Das Script als Administrator/Root ausgeführt wird") + print("- Smart Array Controller vorhanden sind") + + +if __name__ == "__main__": + main() \ No newline at end of file