import reflex as rx import subprocess import os import glob import datetime import asyncio # --- PFADE ABSOLUT DEFINIEREN --- APP_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(APP_DIR) PLAYBOOK_DIR = os.path.join(BASE_DIR, "playbooks") OUTPUT_DIR = os.path.join(BASE_DIR, "outputs") # Sichere Ordner-Erstellung try: os.makedirs(PLAYBOOK_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True) except Exception as e: print(f"Fehler bei der Ordner-Erstellung: {str(e)}") class AppState(rx.State): playbooks: list[str] = [] selected_playbook: str = "" output: str = "" is_running: bool = False error: str = "" # --- NEU: Für die Historie --- past_runs: list[str] = [] selected_run: str = "" def set_selected_playbook(self, val: str): self.selected_playbook = val def set_selected_run(self, val: str): """Setzt den ausgewählten alten Durchlauf und lädt dessen Inhalt.""" self.selected_run = val if val: try: with open(os.path.join(OUTPUT_DIR, val), "r", encoding="utf-8") as f: self.output = f.read() self.error = f"Log geladen: {val}" except Exception as e: self.error = f"Fehler beim Laden des Logs: {str(e)}" async def load_data(self): """Triggert das Laden von Playbooks UND alten Durchläufen beim Seitenstart.""" await self.load_playbooks() await self.load_past_runs() async def load_playbooks(self): """Lädt alle Playbooks aus dem konfigurierten Ordner.""" try: playbook_files = glob.glob(os.path.join(PLAYBOOK_DIR, "*.yml")) + \ glob.glob(os.path.join(PLAYBOOK_DIR, "*.yaml")) self.playbooks = [os.path.basename(p) for p in playbook_files] if self.playbooks and not self.selected_playbook: self.selected_playbook = self.playbooks[0] self.error = "" except Exception as e: self.error = f"Fehler beim Laden der Playbooks: {str(e)}" async def load_past_runs(self): """Scannt den outputs/ Ordner nach alten Logs und sortiert sie (neueste zuerst).""" try: log_files = glob.glob(os.path.join(OUTPUT_DIR, "*.txt")) # Sortiert nach Dateiname absteigend (da Zeitstempel vorne steht, ist das neueste oben) self.past_runs = sorted([os.path.basename(l) for l in log_files], reverse=True) except Exception as e: self.error = f"Fehler beim Laden der Historie: {str(e)}" async def run_playbook(self): """Führt das ausgewählte Playbook asynchron aus und speichert den Output.""" if not self.selected_playbook: self.error = "Bitte wählen Sie ein Playbook aus." return self.is_running = True self.error = "" self.output = "Wird ausgeführt..." yield playbook_path = os.path.join(PLAYBOOK_DIR, self.selected_playbook) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") output_filename = f"{timestamp}_{self.selected_playbook.replace('.yml', '').replace('.yaml', '')}.txt" output_path = os.path.join(OUTPUT_DIR, output_filename) try: proc = await asyncio.create_subprocess_exec( "ansible-playbook", playbook_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300) except asyncio.TimeoutError: proc.kill() await proc.wait() raise asyncio.TimeoutError("Playbook hat das Timeout überschritten.") output_text = stdout.decode() + stderr.decode() self.output = output_text # Output speichern try: with open(output_path, "w", encoding="utf-8") as f: f.write(output_text) # Direkt die Historie aktualisieren, damit das neue Log auftaucht await self.load_past_runs() except Exception as e: self.error = f"Fehler beim Speichern des Outputs: {str(e)}" if proc.returncode != 0: self.error = f"Playbook wurde mit Fehlercode {proc.returncode} beendet." else: self.error = "Playbook erfolgreich ausgeführt." except asyncio.TimeoutError as e: self.error = str(e) self.output = "Fehler: Timeout" except Exception as e: self.error = f"Ein Fehler ist aufgetreten: {str(e)}" self.output = "Fehler bei der Ausführung" finally: self.is_running = False yield def index(): return rx.container( rx.heading("Ansible Playbook Runner", size="6"), # Ein Grid layout: Links Steuerung & Historie, rechts das große Log-Fenster rx.grid( # Linke Spalte (Bedienung) rx.vstack( rx.text("Playbook auswählen:", font_weight="bold"), rx.select( AppState.playbooks, placeholder="Wählen Sie ein Playbook...", value=AppState.selected_playbook, on_change=AppState.set_selected_playbook, width="100%", ), rx.button( "Ausführen", on_click=AppState.run_playbook, disabled=AppState.is_running, color_scheme="blue", width="100%", ), rx.divider(margin_y="1em"), # --- NEU: Historie Dropdown --- rx.text("Alte Durchläufe ansehen:", font_weight="bold"), rx.select( AppState.past_runs, placeholder="Historie wählen...", value=AppState.selected_run, on_change=AppState.set_selected_run, width="100%", ), rx.text( AppState.error, color=rx.cond(AppState.error, "red", "transparent"), margin_top="1em", ), align_items="stretch", spacing="3", ), # Rechte Spalte (Textausgabe) rx.vstack( rx.text("Ausgabe / Log-Inhalt:", font_weight="bold"), rx.text_area( value=AppState.output, width="100%", height="500px", disabled=True, resize="none", ), width="100%", ), columns="2", spacing="5", margin_top="2em", ), padding="2em", max_width="1200px", # Etwas breiter gemacht fürs Grid-Layout margin="auto", ) app = rx.App() # Wichtig: Wir rufen beim Laden jetzt "load_data" auf, um beides zu laden app.add_page(index, route="/", on_load=AppState.load_data) if __name__ == "__main__": try: app.run(debug=True) except Exception as e: print(f"Fehler bei der Ausführung: {str(e)}")