import reflex as rx import subprocess import os import glob import datetime import asyncio import traceback # --- PFADE ABSOLUT DEFINIEREN --- APP_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(APP_DIR) # Expliziter Pfad zum playbooks/ Ordner PLAYBOOK_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "playbooks")) print(f"Playbook directory: {PLAYBOOK_DIR}") 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 = "" show_preview: bool = False preview_content: str = "" def toggle_preview(self): """Schaltet die Vorschau um.""" self.show_preview = not self.show_preview if self.show_preview and self.selected_playbook: self.load_preview() def load_preview(self): """Lädt den Inhalt des ausgewählten Playbooks für die Vorschau.""" if not self.selected_playbook: self.preview_content = "Wählen Sie ein Playbook" return try: playbook_path = os.path.join(PLAYBOOK_DIR, self.selected_playbook) with open(playbook_path, "r", encoding="utf-8") as f: self.preview_content = f.read() except Exception as e: self.preview_content = f"Fehler beim Laden: {str(e)}" past_runs: list[str] = [] selected_run: str = "" def set_selected_playbook(self, val: str): self.selected_playbook = val if self.show_preview: self.load_preview() 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: self.playbooks = [] print(f"Searching for playbooks in: {PLAYBOOK_DIR}") if not os.path.exists(PLAYBOOK_DIR): self.error = f"Playbook directory not found: {PLAYBOOK_DIR}" print(self.error) return playbook_files = [] for ext in ("*.yml", "*.yaml"): playbook_files.extend(glob.glob(os.path.join(PLAYBOOK_DIR, ext))) print(f"Found playbook files: {playbook_files}") self.playbooks = sorted([os.path.basename(p) for p in playbook_files]) if self.playbooks: if not self.selected_playbook or self.selected_playbook not in self.playbooks: self.selected_playbook = self.playbooks[0] else: self.selected_playbook = "" self.error = "Keine Playbooks gefunden. Bitte legen Sie welche im playbooks/ Ordner ab." print(f"Playbooks: {self.playbooks}") print(f"Selected: {self.selected_playbook}") except Exception as e: self.error = f"Fehler beim Laden der Playbooks: {str(e)}" print(f"Fehler in load_playbooks: {str(e)}") print(f"Traceback: {traceback.format_exc()}") async def load_past_runs(self): """Scannt den outputs/ Ordner nach alten Logs, zeigt nur die 20 neuesten an und archiviert den Rest.""" try: archive_dir = os.path.join(OUTPUT_DIR, "archiv") os.makedirs(archive_dir, exist_ok=True) log_files = glob.glob(os.path.join(OUTPUT_DIR, "*.txt")) sorted_files = sorted(log_files, key=os.path.getmtime, reverse=True) for file in sorted_files[20:]: try: dest = os.path.join(archive_dir, os.path.basename(file)) os.rename(file, dest) except Exception as e: print(f"Fehler beim Archivieren von {file}: {str(e)}") remaining_files = glob.glob(os.path.join(OUTPUT_DIR, "*.txt")) self.past_runs = sorted([os.path.basename(l) for l in remaining_files], key=lambda x: x.split("_")[0:2], reverse=True)[:20] except Exception as e: self.error = f"Fehler beim Laden der Historie: {str(e)}" print(f"Fehler in load_past_runs: {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 = f"Starte Ausführung von {self.selected_playbook}..." print(f"Starte Playbook: {self.selected_playbook}") yield playbook_path = os.path.join(PLAYBOOK_DIR, self.selected_playbook) print(f"Playbook Pfad: {playbook_path}") 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 try: with open(output_path, "w", encoding="utf-8") as f: f.write(output_text) 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 = "" self.error = f"{self.selected_playbook} wurde 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"), rx.grid( rx.vstack( rx.text("Playbook auswählen:", font_weight="bold"), rx.vstack( rx.cond( AppState.playbooks, rx.select( AppState.playbooks, placeholder="Wählen Sie ein Playbook...", value=AppState.selected_playbook, on_change=AppState.set_selected_playbook, width="100%", ), rx.text("Keine Playbooks gefunden!", color="red") ), rx.cond( AppState.error, rx.text(AppState.error, color="red"), ), spacing="2" ), rx.button( rx.cond( AppState.is_running, rx.hstack(rx.spinner(), "Wird ausgeführt..."), "Ausführen" ), on_click=AppState.run_playbook, disabled=AppState.is_running, color_scheme="blue", width="100%", ), rx.divider(margin_y="1em"), rx.vstack( rx.button( "Playbook-Vorschau anzeigen", on_click=AppState.toggle_preview, width="100%", font_weight="bold" ), rx.cond( AppState.show_preview, rx.code_block( AppState.preview_content, language="yaml", width="100%", max_height="300px", ) ), width="100%", spacing="2" ), 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.cond( AppState.error, rx.text( AppState.error, color="red", margin_top="1em", font_weight="bold" ), ), align_items="stretch", spacing="3", ), 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="1", sm_columns="1", md_columns="2", spacing="5", margin_top="2em", width="100%", ), padding="2em", max_width="1200px", margin="auto", ) app = rx.App() app.add_page( index, route="/", on_load=AppState.load_data, meta=[] ) if __name__ == "__main__": try: # Nur die benötigten Argumente übergeben; Ports/Hosts kommen aus rxconfig.py app.run( host="0.0.0.0", reload=True, log_level="debug", ) except Exception as e: print(f"Fehler bei der Ausführung: {str(e)}")