From a8b103e2c66e42141f4e196b610606202c57c342 Mon Sep 17 00:00:00 2001 From: jonnybravo Date: Mon, 20 Jul 2026 17:16:55 +0200 Subject: [PATCH] v.0.01 --- .gitignore | 3 + Caddyfile | 1 - app/app.py | 12 +- app/app.py (bleibt unverändert) | 309 ++++++++++++++++++++++++ docker-compose.yml | 9 +- nginx.conf | 43 ++++ outputs/20260717_153841_hello_world.txt | 16 -- outputs/20260717_163106_hello_world.txt | 16 -- outputs/20260717_163214_hello_world.txt | 42 ---- outputs/20260717_163255_hello_world.txt | 21 -- outputs/20260717_163326_hello_world.txt | 8 - outputs/20260717_163348_hello_world.txt | 8 - rxconfig.py | 10 +- start.sh | 5 + 14 files changed, 383 insertions(+), 120 deletions(-) delete mode 100644 Caddyfile create mode 100644 app/app.py (bleibt unverändert) create mode 100644 nginx.conf delete mode 100644 outputs/20260717_153841_hello_world.txt delete mode 100644 outputs/20260717_163106_hello_world.txt delete mode 100644 outputs/20260717_163214_hello_world.txt delete mode 100644 outputs/20260717_163255_hello_world.txt delete mode 100644 outputs/20260717_163326_hello_world.txt delete mode 100644 outputs/20260717_163348_hello_world.txt create mode 100755 start.sh diff --git a/.gitignore b/.gitignore index be46806..894729e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ __pycache__/ +outputs/ *.db .web .states +certs/ +reflex* assets/external/ *.py[cod] .aider* diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 852688b..0000000 --- a/Caddyfile +++ /dev/null @@ -1 +0,0 @@ -# Diese Datei wird für den direkten Host‑Betrieb ohne Caddy nicht benötigt. diff --git a/app/app.py b/app/app.py index 210d50a..3559071 100644 --- a/app/app.py +++ b/app/app.py @@ -31,6 +31,12 @@ class AppState(rx.State): error: str = "" show_preview: bool = False preview_content: str = "" + + @rx.var + def error_color(self) -> str: + if "erfolgreich" in self.error: + return "green" + return "red" def toggle_preview(self): """Schaltet die Vorschau um.""" @@ -65,7 +71,7 @@ class AppState(rx.State): 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}" + self.error = f"Log erfolgreich geladen: {val}" except Exception as e: self.error = f"Fehler beim Laden des Logs: {str(e)}" @@ -210,7 +216,7 @@ def index(): ), rx.cond( AppState.error, - rx.text(AppState.error, color="red"), + rx.text(AppState.error, color=AppState.error_color), ), spacing="2" ), @@ -257,7 +263,7 @@ def index(): AppState.error, rx.text( AppState.error, - color="red", + color=AppState.error_color, margin_top="1em", font_weight="bold" ), diff --git a/app/app.py (bleibt unverändert) b/app/app.py (bleibt unverändert) new file mode 100644 index 0000000..210d50a --- /dev/null +++ b/app/app.py (bleibt unverändert) @@ -0,0 +1,309 @@ +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)}") diff --git a/docker-compose.yml b/docker-compose.yml index b8a7b74..252b8d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1 +1,8 @@ -# Diese Datei wird für den direkten Host‑Betrieb ohne Docker nicht benötigt. +services: + nginx: + image: nginx:alpine + network_mode: host + volumes: + - /home/jonnybravo/test_ansible/nginx.conf:/etc/nginx/nginx.conf:ro,Z + - /home/jonnybravo/test_ansible/certs:/etc/nginx/certs:ro,Z + restart: unless-stopped diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..4b26054 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,43 @@ +events {} + +http { + server { + listen 443 ssl; + server_name localhost man-dan-05; + + ssl_certificate /etc/nginx/certs/man-dan-05.crt; + ssl_certificate_key /etc/nginx/certs/man-dan-05.key; + + # WebSocket endpoint unter /api/_event an das Backend (/_event) weiterleiten + location /api/_event { + proxy_pass http://localhost:3001/_event; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API – Präfix /api entfernen und an Backend weiterleiten + location /api/ { + proxy_pass http://localhost:3001/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Frontend (alles andere) + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/outputs/20260717_153841_hello_world.txt b/outputs/20260717_153841_hello_world.txt deleted file mode 100644 index 545f2db..0000000 --- a/outputs/20260717_153841_hello_world.txt +++ /dev/null @@ -1,16 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** - -TASK [Gathering Facts] ********************************************************* -ok: [localhost] - -TASK [Hallo Welt ausgeben] ***************************************************** -ok: [localhost] => { - "msg": "Hallo Welt" -} - -PLAY RECAP ********************************************************************* -localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' diff --git a/outputs/20260717_163106_hello_world.txt b/outputs/20260717_163106_hello_world.txt deleted file mode 100644 index 545f2db..0000000 --- a/outputs/20260717_163106_hello_world.txt +++ /dev/null @@ -1,16 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** - -TASK [Gathering Facts] ********************************************************* -ok: [localhost] - -TASK [Hallo Welt ausgeben] ***************************************************** -ok: [localhost] => { - "msg": "Hallo Welt" -} - -PLAY RECAP ********************************************************************* -localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' diff --git a/outputs/20260717_163214_hello_world.txt b/outputs/20260717_163214_hello_world.txt deleted file mode 100644 index d90c430..0000000 --- a/outputs/20260717_163214_hello_world.txt +++ /dev/null @@ -1,42 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** - -TASK [Gathering Facts] ********************************************************* -ok: [localhost] - -TASK [Hallo Welt ausgeben] ***************************************************** -ok: [localhost] => { - "msg": "Hallo man-dan-05! Die Uhrzeit ist 2026-07-17T14:32:14Z" -} - -TASK [Uhrzeit ausgeben] ******************************************************** -ok: [localhost] => { - "msg": "Die Uhrzeit ist 2026-07-17T14:32:14Z" -} - -PLAY RECAP ********************************************************************* -localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' -[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg. -[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. -Origin: /home/jonnybravo/test_ansible/playbooks/hello_world.yml:9:12 - -7 - name: Hallo Welt ausgeben -8 debug: -9 msg: "Hallo {{ ansible_hostname }}! Die Uhrzeit ist {{ ansible_date_time.iso8601 }}" - ^ column 12 - -Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. - -[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. -Origin: /home/jonnybravo/test_ansible/playbooks/hello_world.yml:13:12 - -11 - name: Uhrzeit ausgeben -12 debug: -13 msg: "Die Uhrzeit ist {{ ansible_date_time.iso8601 }}" - ^ column 12 - -Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. - diff --git a/outputs/20260717_163255_hello_world.txt b/outputs/20260717_163255_hello_world.txt deleted file mode 100644 index b211b49..0000000 --- a/outputs/20260717_163255_hello_world.txt +++ /dev/null @@ -1,21 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** - -TASK [Gathering Facts] ********************************************************* -ok: [localhost] - -TASK [Hallo Welt ausgeben] ***************************************************** -ok: [localhost] => { - "msg": "Hallo man-dan-05! Die Uhrzeit ist 2026-07-17T14:32:56Z" -} - -TASK [Uhrzeit ausgeben] ******************************************************** -ok: [localhost] => { - "msg": "Die Uhrzeit ist 2026-07-17T14:32:56Z" -} - -PLAY RECAP ********************************************************************* -localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' diff --git a/outputs/20260717_163326_hello_world.txt b/outputs/20260717_163326_hello_world.txt deleted file mode 100644 index 83e0348..0000000 --- a/outputs/20260717_163326_hello_world.txt +++ /dev/null @@ -1,8 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** -skipping: no hosts matched - -PLAY RECAP ********************************************************************* - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' diff --git a/outputs/20260717_163348_hello_world.txt b/outputs/20260717_163348_hello_world.txt deleted file mode 100644 index 83e0348..0000000 --- a/outputs/20260717_163348_hello_world.txt +++ /dev/null @@ -1,8 +0,0 @@ - -PLAY [Hallo Welt ausgeben] ***************************************************** -skipping: no hosts matched - -PLAY RECAP ********************************************************************* - -[WARNING]: No inventory was parsed, only implicit localhost is available -[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' diff --git a/rxconfig.py b/rxconfig.py index c272e08..0a33262 100644 --- a/rxconfig.py +++ b/rxconfig.py @@ -7,14 +7,16 @@ config = rx.Config( app_name="app", frontend_port=3000, backend_port=3001, - api_url="http://localhost:3001", + # Absolute API‑URL über Nginx mit Hostname + api_url="https://man-dan-05/api", backend_host="0.0.0.0", frontend_host="0.0.0.0", - backend_ws_url="ws://localhost:3001/ws", - cors_allowed_origins=["http://localhost:3000"], + # WebSocket über Nginx (wss) mit Hostname und Pfad /api/_event + backend_ws_url="wss://man-dan-05/api/_event", + cors_allowed_origins=["https://localhost", "http://localhost:3000", "https://man-dan-05"], cors_credentials=True, plugins=[RadixThemesPlugin()], disable_plugins=[SitemapPlugin], - vite_allowed_hosts=["localhost", "127.0.0.1"], + vite_allowed_hosts=["localhost", "127.0.0.1", "man-dan-05"], env=rx.Env.DEV, ) diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..8263f1c --- /dev/null +++ b/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Docker-Container starten (nginx) +docker compose up -d +# Reflex ausführen +reflex run