Refactor and streamline project setup and management scripts
- Removed `run.sh` and `test.sh` scripts to simplify the project structure. - Added `setup-first-install.sh` to handle initial setup tasks with options to skip specific steps. - Updated `invario-stack-autostart.service` to use `gitea.sh` for starting the stack on boot. - Removed `start-stack-on-boot.sh` as its functionality is now integrated into the service. - Enhanced `admin_system.html` with a new live logs panel, including automatic updates and log source selection. - Improved CSS styles for the new logs panel for better UI consistency. - Added JavaScript functionality to fetch and display live logs from the server.
This commit is contained in:
+11
-9
@@ -5,18 +5,20 @@ Nach jedem Neustart waren die Subdomains nicht mehr im `/etc/hosts` eingetragen,
|
||||
|
||||
## Lösung
|
||||
|
||||
Diese Lösung bietet **3 Ebenen der Automatisierung**:
|
||||
Diese Lösung bietet drei Ebenen der Automatisierung:
|
||||
|
||||
### 1. ✅ Automatische Sync bei Container-Start
|
||||
**Datei:** `launch_prod.sh` und `launch_dev.sh`
|
||||
Hinweis für neue Systeme: `sudo ./setup-first-install.sh` richtet den Hosts-Sync-Service zusammen mit dem restlichen Basis-Setup automatisch ein.
|
||||
|
||||
### 1. Automatische Sync bei Container-Start
|
||||
Dateien: `Website/launch_prod.sh` und `Website/launch_dev.sh`
|
||||
|
||||
- Nach `docker compose up` wird `sync_dev_hosts.sh` automatisch aufgerufen
|
||||
- Ein Background-Daemon sorgt für regelmäßige Synchronisation
|
||||
- **Development:** Alle 30 Sekunden (konfigurierbar via `DEV_HOSTS_REFRESH_INTERVAL`)
|
||||
- **Production:** Alle 3600 Sekunden/1 Stunde (konfigurierbar via `PROD_HOSTS_REFRESH_INTERVAL`)
|
||||
|
||||
### 2. ✅ systemd Service für permanente Automatisierung
|
||||
**Dateien:** `invario-hosts-sync.service`, `setup-hosts-sync.sh`
|
||||
### 2. systemd Service für permanente Automatisierung
|
||||
Dateien: `invario-hosts-sync.service`, `setup-hosts-sync.sh`
|
||||
|
||||
Der systemd-Service stellt sicher, dass die Hosts-Synchronisation auch nach System-Reboots aktiviert bleibt.
|
||||
|
||||
@@ -44,7 +46,7 @@ sudo rm /etc/systemd/system/invario-hosts-sync.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
### 3. ✅ Manuelle Synchronisation
|
||||
### 3. Manuelle Synchronisation
|
||||
Jederzeit manuell möglich:
|
||||
```bash
|
||||
cd /home/max/Dokumente/repos/Key-service-Server/Website
|
||||
@@ -89,7 +91,7 @@ sudo systemctl restart invario-hosts-sync
|
||||
|
||||
### Hosts werden nicht aktualisiert
|
||||
```bash
|
||||
# Manuell test
|
||||
# Manuell testen
|
||||
./Website/sync_dev_hosts.sh
|
||||
|
||||
# MongoDB-Verbindung prüfen
|
||||
@@ -97,8 +99,8 @@ docker compose ps mongodb
|
||||
docker compose logs mongodb
|
||||
```
|
||||
|
||||
### Berechtigungen-Fehler
|
||||
Das Script benötigt Zugriff auf `/etc/hosts`. Normalerweise wird es als `root` ausgeführt:
|
||||
### Berechtigungen
|
||||
Das Skript benötigt Zugriff auf `/etc/hosts`. Normalerweise wird es als `root` ausgeführt:
|
||||
```bash
|
||||
# Wenn nötig mit sudo
|
||||
sudo ./Website/sync_dev_hosts.sh
|
||||
|
||||
@@ -1,173 +1,119 @@
|
||||
# Key Service Server
|
||||
|
||||
Dieses Repo nutzt zwei zentrale Shell-Skripte:
|
||||
|
||||
- `gitea.sh`: Start/Stop/Status der Docker-Services (Gitea + Website)
|
||||
- `nginx.sh`: Nginx Reverse Proxy, SSL-Zertifikate und Aktivierung/Deaktivierung der Sites
|
||||
Dieses Repository ist auf einen schlanken, Docker-first Betrieb ausgerichtet.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Docker + Docker Compose Plugin
|
||||
- Docker Engine + Docker Compose Plugin
|
||||
- Nginx
|
||||
- OpenSSL
|
||||
- Optional: UFW (wird automatisch fuer 80/443 konfiguriert, wenn aktiv)
|
||||
- Optional: UFW (Ports 80/443 werden bei aktivem UFW automatisch freigegeben)
|
||||
|
||||
## 1) Docker Services (`gitea.sh`)
|
||||
## Script Matrix
|
||||
|
||||
### Core-Skripte (aktiv)
|
||||
|
||||
- [gitea.sh](gitea.sh)
|
||||
- Start/Stop/Restart/Status für Gitea + Website-Stack
|
||||
- [nginx.sh](nginx.sh)
|
||||
- Nginx-Konfiguration anwenden/deaktivieren
|
||||
- [Website/provision_instance.sh](Website/provision_instance.sh)
|
||||
- Instanz-Provisioning für Inventarsystem (release-basiert, Multiinstancing-aware)
|
||||
- [Website/launch_dev.sh](Website/launch_dev.sh)
|
||||
- Dev-Start der Website inkl. Host-Sync
|
||||
- [Website/launch_prod.sh](Website/launch_prod.sh)
|
||||
- Prod-Start der Website inkl. Host-Sync
|
||||
- [Website/start_docker_build.sh](Website/start_docker_build.sh)
|
||||
- Build/Start-Helfer für Website-Container
|
||||
- [Website/sync_dev_hosts.sh](Website/sync_dev_hosts.sh)
|
||||
- Synchronisiert dynamische Subdomains in /etc/hosts
|
||||
|
||||
### Setup-Skripte (optional, aktiv)
|
||||
|
||||
- [setup-first-install.sh](setup-first-install.sh)
|
||||
- Führt Erstinstallation in einem Ablauf aus (Start, Nginx, Hosts-Sync-Service, Autostart-Service)
|
||||
- [setup-hosts-sync.sh](setup-hosts-sync.sh)
|
||||
- Installiert den systemd-Service für automatische Hosts-Synchronisierung
|
||||
- [setup-stack-autostart.sh](setup-stack-autostart.sh)
|
||||
- Installiert den systemd-Service für Stack-Autostart über gitea.sh
|
||||
|
||||
### Legacy-Skripte (entfernt)
|
||||
|
||||
- start-stack-on-boot.sh (durch direkten systemd-ExecStart auf gitea.sh ersetzt)
|
||||
- provision_instance.sh im Repo-Root (Duplikat, Website-Version ist Single Source)
|
||||
- Website/run.sh (alt, nicht Teil des aktuellen Flows)
|
||||
- Website/test.sh (alt, nicht Teil des aktuellen Flows)
|
||||
|
||||
## Core Betrieb
|
||||
|
||||
### Docker Services
|
||||
|
||||
Datei: [gitea.sh](gitea.sh)
|
||||
|
||||
### Befehle
|
||||
Befehle:
|
||||
|
||||
- `sudo ./gitea.sh start`
|
||||
- Erstellt/aktualisiert Gitea Compose unter `/opt/gitea-server/docker-compose.yml`
|
||||
- Startet Gitea und Website Docker Services
|
||||
- Schreibt Runtime-Werte nach `/opt/gitea-server/runtime.env` fuer Nginx
|
||||
- sudo ./gitea.sh start
|
||||
- sudo ./gitea.sh stop
|
||||
- sudo ./gitea.sh restart
|
||||
- sudo ./gitea.sh status
|
||||
|
||||
- `sudo ./gitea.sh stop`
|
||||
- Stoppt Gitea und Website Docker Services
|
||||
Wichtige Ports:
|
||||
|
||||
- `sudo ./gitea.sh restart`
|
||||
- Entspricht `stop` gefolgt von `start`
|
||||
- Gitea intern: 127.0.0.1:3001
|
||||
- Gitea SSH: 2222
|
||||
- Website Upstream: 4999
|
||||
|
||||
- `sudo ./gitea.sh status`
|
||||
- Zeigt laufende Container fuer Gitea/Website an
|
||||
|
||||
### Wichtige Ports
|
||||
|
||||
- Gitea intern: `127.0.0.1:3001` (fix)
|
||||
- Gitea SSH: `2222`
|
||||
- Website Upstream: `4999`
|
||||
|
||||
## 2) Nginx (`nginx.sh`)
|
||||
### Nginx
|
||||
|
||||
Datei: [nginx.sh](nginx.sh)
|
||||
|
||||
### Befehle
|
||||
Befehle:
|
||||
|
||||
- `sudo ./nginx.sh apply`
|
||||
- Liest Runtime-Werte aus `/opt/gitea-server/runtime.env`
|
||||
- Erstellt Nginx Site-Konfigurationen fuer:
|
||||
- `update.meine-domain` -> Gitea
|
||||
- `meine-domain` -> Website
|
||||
- Prueft Zertifikate im Ordner `/etc/nginx/certs`
|
||||
- Falls Zertifikate fehlen: erzeugt self-signed Zertifikate automatisch
|
||||
- Testet Nginx-Konfiguration und laedt Nginx neu
|
||||
- sudo ./nginx.sh apply
|
||||
- sudo ./nginx.sh disable
|
||||
- sudo ./nginx.sh deactivate
|
||||
|
||||
- `sudo ./nginx.sh disable`
|
||||
- `sudo ./nginx.sh deactivate`
|
||||
- Deaktiviert die verwalteten Nginx Sites (`gitea`, `website`)
|
||||
- Testet und laedt Nginx neu
|
||||
|
||||
## Typischer Ablauf
|
||||
|
||||
1. Docker Services starten:
|
||||
|
||||
```bash
|
||||
sudo ./gitea.sh start
|
||||
```
|
||||
|
||||
2. Nginx konfigurieren/aktivieren:
|
||||
|
||||
```bash
|
||||
sudo ./nginx.sh apply
|
||||
```
|
||||
|
||||
3. Status pruefen:
|
||||
|
||||
```bash
|
||||
sudo ./gitea.sh status
|
||||
```
|
||||
|
||||
4. Nginx bei Bedarf deaktivieren:
|
||||
|
||||
```bash
|
||||
sudo ./nginx.sh disable
|
||||
```
|
||||
|
||||
## Nginx Templates
|
||||
|
||||
Die Nginx-Vorlagen liegen unter [nginx](nginx):
|
||||
Nginx-Templates:
|
||||
|
||||
- [nginx/gitea.http.conf.template](nginx/gitea.http.conf.template)
|
||||
- [nginx/gitea.https.conf.template](nginx/gitea.https.conf.template)
|
||||
- [nginx/website.http.conf.template](nginx/website.http.conf.template)
|
||||
- [nginx/website.https.conf.template](nginx/website.https.conf.template)
|
||||
|
||||
## Admin: Schul-Instanzen fuer Subdomains
|
||||
## Inventarsystem Instanzen
|
||||
|
||||
Die Website hat jetzt eine Admin-Seite zum Starten von Inventarsystem-Instanzen pro Schule:
|
||||
Admin-Oberfläche:
|
||||
|
||||
- Route: `/admin/instances`
|
||||
- Admin-Menue: `Schul-Instanzen`
|
||||
- Ziel-Repository (Standard): `https://github.com/AIIrondev/legendary-octo-garbanzo`
|
||||
- Route: /admin/instances
|
||||
- Menu: Schul-Instanzen
|
||||
|
||||
### Ablauf
|
||||
Provisioning-Flow:
|
||||
|
||||
1. Admin gibt Schulname (und optional Subdomain) ein.
|
||||
2. Admin weist die Instanz einem konkreten Nutzer zu.
|
||||
3. Admin wählt eine Version (Image-Tag) pro Nutzer/Instanz.
|
||||
4. Admin kann das Bibliotheksmodul für die Instanz aktivieren/deaktivieren.
|
||||
5. Backend ruft `provision_instance.sh` auf.
|
||||
6. Skript klont/aktualisiert die Instanz unter dem Basisverzeichnis.
|
||||
7. Skript startet die Instanz (`start.sh --no-cron`) und versucht eine Nginx-Site fuer die Subdomain anzulegen.
|
||||
8. Status wird in MongoDB (`school_instances`) gespeichert und im Admin-Fenster angezeigt.
|
||||
1. Admin wählt Nutzer, Schule, Version und optional Subdomain.
|
||||
2. Backend ruft [Website/provision_instance.sh](Website/provision_instance.sh) auf.
|
||||
3. Provisioning installiert release-basiert aus GitHub-Releases.
|
||||
4. Wenn verfügbar, wird docker-compose-multitenant.yml bevorzugt genutzt.
|
||||
5. Instanzstatus wird in school_instances gespeichert.
|
||||
|
||||
Hinweis: Die Zuweisung ist auf `eine Instanz pro Nutzer` begrenzt.
|
||||
Wichtige Website-Variablen:
|
||||
|
||||
### Wichtige Environment-Variablen (Website)
|
||||
- INSTANCE_REPO_URL (default: https://github.com/AIIrondev/legendary-octo-garbanzo)
|
||||
- INSTANCE_PARENT_DOMAIN (default: meine-domain)
|
||||
- INSTANCE_BASE_DIR (default: /opt/inventarsystem-instances)
|
||||
- INSTANCE_PROVISION_SCRIPT (default: Website/provision_instance.sh)
|
||||
|
||||
- `INSTANCE_REPO_URL` (default: `https://github.com/AIIrondev/legendary-octo-garbanzo`)
|
||||
- `INSTANCE_PARENT_DOMAIN` (default: `meine-domain`)
|
||||
- `INSTANCE_BASE_DIR` (default: `/opt/inventarsystem-instances`)
|
||||
- `INSTANCE_PROVISION_SCRIPT` (default: `Website/provision_instance.sh`)
|
||||
Berechtigungen für Provisioning:
|
||||
|
||||
### Berechtigungen
|
||||
- Docker-Zugriff
|
||||
- Schreibrechte auf /etc/nginx/sites-available und /etc/nginx/sites-enabled
|
||||
- nginx -t und nginx reload/signal-Rechte
|
||||
|
||||
Das Provisioning-Skript fuehrt Docker- und Nginx-Operationen aus. Der Prozess, der die Website ausfuehrt, braucht daher:
|
||||
## Betriebsmodi Website
|
||||
|
||||
- Zugriff auf `docker` (Docker Socket / Gruppe)
|
||||
- Schreibrechte auf `/etc/nginx/sites-available` und `/etc/nginx/sites-enabled`
|
||||
- Recht zum `nginx -t` und `nginx -s reload`
|
||||
- Development: [Website/launch_dev.sh](Website/launch_dev.sh)
|
||||
- Production: [Website/launch_prod.sh](Website/launch_prod.sh)
|
||||
|
||||
Wenn diese Rechte fehlen, wird die Instanz ggf. trotzdem gestartet, aber die Nginx-Verknuepfung muss manuell erfolgen.
|
||||
|
||||
### Nutzerfenster: Eigene Instanz verwalten
|
||||
|
||||
Eingeloggte Nutzer haben im Nutzerbereich jetzt den Punkt `Meine Instanz`.
|
||||
|
||||
- Route: `/my/instance`
|
||||
- Funktion: Nur zugewiesene Instanz einsehen und nutzen (read-only)
|
||||
- Zuordnung: ueber `owner_username`
|
||||
|
||||
## Admin: System-Tools
|
||||
|
||||
Neue Admin-Seite fuer Betriebsaufgaben:
|
||||
|
||||
- Route: `/admin/system`
|
||||
- Funktionen:
|
||||
- Core-Services neu starten (`website`, `mongodb`)
|
||||
- Core-Logs als Datei herunterladen
|
||||
- Pro Instanz: Backup, Update, Restart
|
||||
- Pro Instanz: Logs als Datei herunterladen
|
||||
|
||||
## Betriebsmodi: Development und Production
|
||||
|
||||
Im Ordner `Website/` gibt es zwei Launcher-Skripte:
|
||||
|
||||
- `./launch_dev.sh`
|
||||
- setzt `INSTANCE_TLS_MODE=development`
|
||||
- erzeugt pro Subdomain self-signed Zertifikate (wenn moeglich)
|
||||
- synchronisiert die Dev-Hosts jetzt beim Start und danach regelmaessig neu
|
||||
- setzt `SESSION_COOKIE_SECURE=0`
|
||||
|
||||
- `./launch_prod.sh`
|
||||
- setzt `INSTANCE_TLS_MODE=production`
|
||||
- nutzt Wildcard-Zertifikat ueber:
|
||||
- `INSTANCE_WILDCARD_CERT_FILE`
|
||||
- `INSTANCE_WILDCARD_KEY_FILE`
|
||||
- setzt `SESSION_COOKIE_SECURE=1`
|
||||
|
||||
Beispiel Production-Start mit eigener Domain:
|
||||
Beispiel Production:
|
||||
|
||||
```bash
|
||||
cd Website
|
||||
@@ -175,4 +121,32 @@ export INSTANCE_PARENT_DOMAIN=example.de
|
||||
export INSTANCE_WILDCARD_CERT_FILE=/etc/nginx/certs/wildcard.example.de.crt
|
||||
export INSTANCE_WILDCARD_KEY_FILE=/etc/nginx/certs/wildcard.example.de.key
|
||||
./launch_prod.sh
|
||||
```
|
||||
```
|
||||
|
||||
## Empfohlener Ablauf
|
||||
|
||||
1. sudo ./gitea.sh start
|
||||
2. sudo ./nginx.sh apply
|
||||
3. sudo ./gitea.sh status
|
||||
|
||||
## Erstinstallation (automatisiert)
|
||||
|
||||
Für eine neue Maschine kann die Erstinstallation in einem Schritt ausgeführt werden:
|
||||
|
||||
```bash
|
||||
sudo ./setup-first-install.sh
|
||||
```
|
||||
|
||||
Standardmäßig macht das Skript:
|
||||
|
||||
1. Startet den Stack über gitea.sh.
|
||||
2. Wendet nginx.sh apply an.
|
||||
3. Installiert den Hosts-Sync-systemd-Service.
|
||||
4. Installiert den Stack-Autostart-systemd-Service.
|
||||
|
||||
Optionen:
|
||||
|
||||
- --skip-start
|
||||
- --skip-nginx
|
||||
- --skip-hosts-sync
|
||||
- --skip-autostart
|
||||
+154
-54
@@ -820,6 +820,30 @@ def _run_command(command: list[str], cwd: str | None = None, timeout: int = 900)
|
||||
return True, output or "OK"
|
||||
|
||||
|
||||
def _instance_compose_file(instance_dir: str) -> str | None:
|
||||
preferred = os.path.join(instance_dir, "docker-compose-multitenant.yml")
|
||||
fallback = os.path.join(instance_dir, "docker-compose.yml")
|
||||
|
||||
if os.path.isfile(preferred):
|
||||
return preferred
|
||||
if os.path.isfile(fallback):
|
||||
return fallback
|
||||
return None
|
||||
|
||||
|
||||
def _instance_compose_cmd(instance_dir: str, args: list[str]) -> list[str] | None:
|
||||
compose_file = _instance_compose_file(instance_dir)
|
||||
if not compose_file:
|
||||
return None
|
||||
|
||||
cmd = ["docker", "compose", "-f", compose_file]
|
||||
env_file = os.path.join(instance_dir, ".docker-build.env")
|
||||
if os.path.isfile(env_file):
|
||||
cmd.extend(["--env-file", ".docker-build.env"])
|
||||
cmd.extend(args)
|
||||
return cmd
|
||||
|
||||
|
||||
def _collect_command_candidates(base_binaries: list[str], args: list[str]) -> list[list[str]]:
|
||||
candidates: list[list[str]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
@@ -943,21 +967,109 @@ def _collect_core_logs() -> tuple[bool, str]:
|
||||
return _run_command(command, cwd=BASE_DIR, timeout=180)
|
||||
|
||||
|
||||
def _truncate_log_blob(text: str, max_lines: int = 220, max_chars: int = 32000) -> str:
|
||||
rows = (text or "").splitlines()
|
||||
if len(rows) > max_lines:
|
||||
rows = rows[-max_lines:]
|
||||
clipped = "\n".join(rows).strip()
|
||||
if len(clipped) > max_chars:
|
||||
clipped = clipped[-max_chars:]
|
||||
return clipped or "Keine Ausgabe"
|
||||
|
||||
|
||||
def _run_first_success(candidates: list[list[str]], cwd: str | None = None, timeout: int = 120) -> tuple[bool, str, str]:
|
||||
if not candidates:
|
||||
return False, "Kein Befehl verfügbar.", ""
|
||||
|
||||
failures: list[str] = []
|
||||
for command in candidates:
|
||||
ok, output = _run_command(command, cwd=cwd, timeout=timeout)
|
||||
if ok:
|
||||
return True, output, " ".join(command)
|
||||
failures.append(f"{' '.join(command)} -> {_tail_output(output, 3)}")
|
||||
|
||||
return False, " | ".join(failures[-2:]) if failures else "Kein Detail vorhanden.", ""
|
||||
|
||||
|
||||
def _collect_core_live_logs(lines: int = 220) -> dict:
|
||||
compose_file = os.path.join(BASE_DIR, "docker-compose.yml")
|
||||
command = [
|
||||
"docker",
|
||||
"compose",
|
||||
"-f",
|
||||
compose_file,
|
||||
"logs",
|
||||
"--no-color",
|
||||
"--tail",
|
||||
str(max(lines, 20)),
|
||||
"website",
|
||||
"mongodb",
|
||||
]
|
||||
ok, output = _run_command(command, cwd=BASE_DIR, timeout=180)
|
||||
return {
|
||||
"ok": ok,
|
||||
"label": "Docker Compose (website, mongodb)",
|
||||
"logs": _truncate_log_blob(output),
|
||||
}
|
||||
|
||||
|
||||
def _collect_systemd_service_snapshot(service_name: str, lines: int = 160) -> dict:
|
||||
status_ok, status_out, status_cmd = _run_first_success(
|
||||
_collect_command_candidates(["systemctl"], ["is-active", service_name]),
|
||||
timeout=60,
|
||||
)
|
||||
status = (status_out or "").strip().splitlines()[-1] if status_ok else "unavailable"
|
||||
|
||||
logs_ok, logs_out, logs_cmd = _run_first_success(
|
||||
_collect_command_candidates(
|
||||
["journalctl"],
|
||||
["-u", service_name, "--no-pager", "-n", str(max(lines, 40))],
|
||||
),
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
if logs_ok:
|
||||
logs_text = _truncate_log_blob(logs_out)
|
||||
else:
|
||||
logs_text = _truncate_log_blob(f"Service-Logs konnten nicht geladen werden.\n{logs_out}")
|
||||
|
||||
return {
|
||||
"service": service_name,
|
||||
"status": status,
|
||||
"status_ok": status_ok,
|
||||
"status_command": status_cmd,
|
||||
"logs_ok": logs_ok,
|
||||
"logs_command": logs_cmd,
|
||||
"logs": logs_text,
|
||||
}
|
||||
|
||||
|
||||
def _collect_homepage_service_logs() -> dict:
|
||||
services = [
|
||||
"invario-hosts-sync.service",
|
||||
"invario-stack-autostart.service",
|
||||
"nginx.service",
|
||||
]
|
||||
snapshots = [_collect_systemd_service_snapshot(service) for service in services]
|
||||
return {
|
||||
"generated_at": _utc_now_iso(),
|
||||
"core": _collect_core_live_logs(),
|
||||
"services": snapshots,
|
||||
}
|
||||
|
||||
|
||||
def _collect_instance_logs(subdomain: str) -> tuple[bool, str]:
|
||||
instance_dir = _resolve_instance_dir(subdomain)
|
||||
if not instance_dir:
|
||||
return False, "Instanzverzeichnis nicht gefunden."
|
||||
|
||||
command = [
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".docker-build.env",
|
||||
"logs",
|
||||
"--no-color",
|
||||
"--tail",
|
||||
"500",
|
||||
]
|
||||
command = _instance_compose_cmd(
|
||||
instance_dir,
|
||||
["logs", "--no-color", "--tail", "500"],
|
||||
)
|
||||
if not command:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
|
||||
return _run_command(command, cwd=instance_dir, timeout=180)
|
||||
|
||||
|
||||
@@ -1096,25 +1208,13 @@ def _set_instance_library_enabled(instance_dir: str, enabled: bool) -> tuple[boo
|
||||
|
||||
|
||||
def _restart_instance_stack(instance_dir: str) -> tuple[bool, str]:
|
||||
restart_cmd = [
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".docker-build.env",
|
||||
"restart",
|
||||
"app",
|
||||
"nginx",
|
||||
"mongodb",
|
||||
]
|
||||
up_cmd = [
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".docker-build.env",
|
||||
"up",
|
||||
"-d",
|
||||
"--remove-orphans",
|
||||
]
|
||||
restart_cmd = _instance_compose_cmd(instance_dir, ["restart", "app", "nginx", "mongodb", "redis"])
|
||||
if not restart_cmd:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
|
||||
up_cmd = _instance_compose_cmd(instance_dir, ["up", "-d", "--remove-orphans"])
|
||||
if not up_cmd:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
|
||||
# restart may fail for stopped services; ensure desired state with up -d afterwards.
|
||||
_run_command(restart_cmd, cwd=instance_dir, timeout=420)
|
||||
@@ -1144,19 +1244,14 @@ def _delete_instance_stack(subdomain: str) -> tuple[bool, str]:
|
||||
details: list[str] = []
|
||||
|
||||
if os.path.isdir(target_dir):
|
||||
compose_file = os.path.join(target_dir, "docker-compose.yml")
|
||||
if os.path.isfile(compose_file):
|
||||
down_cmd = [
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".docker-build.env",
|
||||
"down",
|
||||
"--remove-orphans",
|
||||
"--volumes",
|
||||
"--timeout",
|
||||
"40",
|
||||
]
|
||||
compose_file = _instance_compose_file(target_dir)
|
||||
if compose_file:
|
||||
down_cmd = _instance_compose_cmd(
|
||||
target_dir,
|
||||
["down", "--remove-orphans", "--volumes", "--timeout", "40"],
|
||||
)
|
||||
if not down_cmd:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
down_ok, down_out = _run_command(down_cmd, cwd=target_dir, timeout=900)
|
||||
if not down_ok:
|
||||
return False, f"Docker-Stack konnte nicht gestoppt werden.\n{_tail_output(down_out, 12)}"
|
||||
@@ -1269,20 +1364,17 @@ def _create_instance_admin_user(
|
||||
"}"
|
||||
)
|
||||
|
||||
up_ok, up_out = _run_command(
|
||||
["docker", "compose", "--env-file", ".docker-build.env", "up", "-d", "mongodb"],
|
||||
cwd=instance_dir,
|
||||
timeout=180,
|
||||
)
|
||||
up_cmd = _instance_compose_cmd(instance_dir, ["up", "-d", "mongodb"])
|
||||
if not up_cmd:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
|
||||
up_ok, up_out = _run_command(up_cmd, cwd=instance_dir, timeout=180)
|
||||
if not up_ok:
|
||||
return False, f"MongoDB der Instanz konnte nicht gestartet werden: {_tail_output(up_out, 10)}"
|
||||
|
||||
ok, output = _run_command(
|
||||
exec_cmd = _instance_compose_cmd(
|
||||
instance_dir,
|
||||
[
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".docker-build.env",
|
||||
"exec",
|
||||
"-T",
|
||||
"mongodb",
|
||||
@@ -1292,9 +1384,11 @@ def _create_instance_admin_user(
|
||||
eval_script,
|
||||
db_name,
|
||||
],
|
||||
cwd=instance_dir,
|
||||
timeout=240,
|
||||
)
|
||||
if not exec_cmd:
|
||||
return False, "Compose-Datei der Instanz wurde nicht gefunden."
|
||||
|
||||
ok, output = _run_command(exec_cmd, cwd=instance_dir, timeout=240)
|
||||
|
||||
if not ok:
|
||||
return False, f"Instanz-Admin konnte nicht angelegt werden: {_tail_output(output, 12)}"
|
||||
@@ -2224,6 +2318,12 @@ def admin_system_stats():
|
||||
return jsonify(_build_server_management_snapshot(instances))
|
||||
|
||||
|
||||
@app.route('/admin/system/logs/live')
|
||||
@admin_required
|
||||
def admin_system_live_logs():
|
||||
return jsonify(_collect_homepage_service_logs())
|
||||
|
||||
|
||||
@app.route('/admin/system/logs/core')
|
||||
@admin_required
|
||||
def admin_download_core_logs():
|
||||
|
||||
+279
-18
@@ -17,6 +17,9 @@ WILDCARD_KEY_FILE="/etc/nginx/certs/wildcard.meine-domain.key"
|
||||
NGINX_SITES_AVAILABLE="/etc/nginx/sites-available"
|
||||
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled"
|
||||
|
||||
RELEASE_BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
|
||||
RELEASE_IMAGE_ASSET_PREFIX="inventarsystem-image-"
|
||||
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
@@ -84,9 +87,219 @@ NUITKA_BUILD=0
|
||||
INVENTAR_HTTP_PORT=$http_port
|
||||
INVENTAR_HTTPS_PORT=$https_port
|
||||
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$app_image_tag
|
||||
INVENTAR_MULTITENANT_ENABLED=true
|
||||
INVENTAR_SESSION_BACKEND=redis
|
||||
INVENTAR_REDIS_HOST=redis
|
||||
INVENTAR_REDIS_PORT=6379
|
||||
INVENTAR_QUERY_CACHE_ENABLED=true
|
||||
EOF
|
||||
}
|
||||
|
||||
preferred_compose_file() {
|
||||
if [ -f "docker-compose-multitenant.yml" ]; then
|
||||
printf '%s' "docker-compose-multitenant.yml"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "docker-compose.yml"
|
||||
}
|
||||
|
||||
repo_slug_from_url() {
|
||||
local repo_url="$1"
|
||||
local normalized=""
|
||||
|
||||
normalized="$repo_url"
|
||||
normalized="${normalized#https://github.com/}"
|
||||
normalized="${normalized#http://github.com/}"
|
||||
normalized="${normalized#git@github.com:}"
|
||||
normalized="${normalized%.git}"
|
||||
|
||||
if [[ "$normalized" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
|
||||
printf '%s' "$normalized"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
fetch_release_metadata() {
|
||||
local repo_slug="$1"
|
||||
local requested_tag="$2"
|
||||
local out_file="$3"
|
||||
local api_url=""
|
||||
|
||||
if [ "$requested_tag" = "latest" ]; then
|
||||
api_url="https://api.github.com/repos/$repo_slug/releases/latest"
|
||||
else
|
||||
api_url="https://api.github.com/repos/$repo_slug/releases/tags/$requested_tag"
|
||||
fi
|
||||
|
||||
if ! curl -fsSL "$api_url" -o "$out_file"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
extract_release_info() {
|
||||
local meta_file="$1"
|
||||
|
||||
python3 - <<'PY' "$meta_file" "$RELEASE_BUNDLE_ASSET" "$RELEASE_IMAGE_ASSET_PREFIX"
|
||||
import json
|
||||
import sys
|
||||
|
||||
meta_file, bundle_asset, image_prefix = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
with open(meta_file, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
tag = (data.get("tag_name") or "").strip()
|
||||
bundle_url = ""
|
||||
image_url = ""
|
||||
|
||||
for asset in data.get("assets", []):
|
||||
name = (asset.get("name") or "").strip()
|
||||
url = (asset.get("browser_download_url") or "").strip()
|
||||
if not url:
|
||||
continue
|
||||
if name == bundle_asset:
|
||||
bundle_url = url
|
||||
if tag and name == f"{image_prefix}{tag}.tar.gz":
|
||||
image_url = url
|
||||
|
||||
print(tag)
|
||||
print(bundle_url)
|
||||
print(image_url)
|
||||
PY
|
||||
}
|
||||
|
||||
pin_compose_app_image() {
|
||||
local instance_dir="$1"
|
||||
local resolved_tag="$2"
|
||||
local compose_file=""
|
||||
|
||||
for compose_file in "$instance_dir/docker-compose.yml" "$instance_dir/docker-compose-multitenant.yml"; do
|
||||
[ -f "$compose_file" ] || continue
|
||||
|
||||
python3 - <<'PY' "$compose_file" "$resolved_tag"
|
||||
import re
|
||||
import sys
|
||||
|
||||
compose_file, tag = sys.argv[1], sys.argv[2]
|
||||
target_image = f"ghcr.io/aiirondev/legendary-octo-garbanzo:{tag}"
|
||||
|
||||
with open(compose_file, "r", encoding="utf-8") as fh:
|
||||
lines = fh.readlines()
|
||||
|
||||
out = []
|
||||
in_app = False
|
||||
in_build = False
|
||||
image_set = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.lstrip(" ")
|
||||
indent = len(line) - len(stripped)
|
||||
|
||||
if not in_app and re.match(r"^\s{2}app:\s*$", line):
|
||||
in_app = True
|
||||
image_set = False
|
||||
out.append(line)
|
||||
out.append(f" image: {target_image}\n")
|
||||
continue
|
||||
|
||||
if in_app:
|
||||
if indent == 2 and re.match(r"^[A-Za-z0-9_-]+:\s*$", stripped):
|
||||
in_app = False
|
||||
in_build = False
|
||||
|
||||
if in_app:
|
||||
if in_build:
|
||||
if indent > 4:
|
||||
continue
|
||||
in_build = False
|
||||
|
||||
if re.match(r"^\s{4}build:\s*$", line):
|
||||
in_build = True
|
||||
continue
|
||||
|
||||
if re.match(r"^\s{4}image:\s*", line):
|
||||
if image_set:
|
||||
continue
|
||||
out.append(f" image: {target_image}\n")
|
||||
image_set = True
|
||||
continue
|
||||
|
||||
out.append(line)
|
||||
|
||||
with open(compose_file, "w", encoding="utf-8") as fh:
|
||||
fh.writelines(out)
|
||||
PY
|
||||
done
|
||||
}
|
||||
|
||||
install_from_release() {
|
||||
local target_dir="$1"
|
||||
local repo_url="$2"
|
||||
local requested_tag="$3"
|
||||
local tmp_dir=""
|
||||
local repo_slug=""
|
||||
local meta_file=""
|
||||
local bundle_path=""
|
||||
local image_path=""
|
||||
local tag=""
|
||||
local bundle_url=""
|
||||
local image_url=""
|
||||
|
||||
repo_slug="$(repo_slug_from_url "$repo_url")" || fail "Repository-URL nicht unterstützbar für Release-Install: $repo_url"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
|
||||
meta_file="$tmp_dir/release.json"
|
||||
|
||||
if ! fetch_release_metadata "$repo_slug" "$requested_tag" "$meta_file"; then
|
||||
fail "Release-Metadaten konnten nicht geladen werden (Repo: $repo_slug, Tag: $requested_tag)."
|
||||
fi
|
||||
|
||||
mapfile -t release_info < <(extract_release_info "$meta_file")
|
||||
tag="${release_info[0]:-}"
|
||||
bundle_url="${release_info[1]:-}"
|
||||
image_url="${release_info[2]:-}"
|
||||
|
||||
[ -n "$tag" ] || fail "Release-Metadaten enthalten keinen gültigen Tag."
|
||||
[ -n "$bundle_url" ] || fail "Release-Asset fehlt: $RELEASE_BUNDLE_ASSET"
|
||||
[ -n "$image_url" ] || fail "Release-Image-Asset fehlt: ${RELEASE_IMAGE_ASSET_PREFIX}${tag}.tar.gz"
|
||||
|
||||
bundle_path="$tmp_dir/$RELEASE_BUNDLE_ASSET"
|
||||
image_path="$tmp_dir/${RELEASE_IMAGE_ASSET_PREFIX}${tag}.tar.gz"
|
||||
|
||||
curl -fsSL "$bundle_url" -o "$bundle_path" || fail "Release-Bundle konnte nicht geladen werden."
|
||||
curl -fsSL "$image_url" -o "$image_path" || fail "Release-Image konnte nicht geladen werden."
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
tar -xzf "$bundle_path" -C "$target_dir" || fail "Release-Bundle konnte nicht entpackt werden."
|
||||
docker load -i "$image_path" >/dev/null 2>&1 || fail "Docker-Image konnte nicht geladen werden."
|
||||
docker tag "ghcr.io/aiirondev/legendary-octo-garbanzo:$tag" "ghcr.io/aiirondev/legendary-octo-garbanzo:latest" >/dev/null 2>&1 || true
|
||||
|
||||
pin_compose_app_image "$target_dir" "$tag"
|
||||
rm -rf "$tmp_dir" >/dev/null 2>&1 || true
|
||||
print_kv "APP_IMAGE_TAG" "$tag"
|
||||
}
|
||||
|
||||
ensure_requested_app_image() {
|
||||
local app_image_tag="$1"
|
||||
local app_image="ghcr.io/aiirondev/legendary-octo-garbanzo:$app_image_tag"
|
||||
|
||||
if docker image inspect "$app_image" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! docker pull "$app_image" >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
set_library_enabled() {
|
||||
local instance_dir="$1"
|
||||
local enabled="$2"
|
||||
@@ -122,13 +335,16 @@ PY
|
||||
}
|
||||
|
||||
normalize_instance_compose() {
|
||||
local compose_file="docker-compose.yml"
|
||||
[ -f "$compose_file" ] || return 0
|
||||
local compose_file=""
|
||||
|
||||
python3 - <<'PY'
|
||||
for compose_file in "docker-compose.yml" "docker-compose-multitenant.yml"; do
|
||||
[ -f "$compose_file" ] || continue
|
||||
|
||||
python3 - <<'PY' "$compose_file"
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path("docker-compose.yml")
|
||||
path = Path(sys.argv[1])
|
||||
content = path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
|
||||
@@ -185,22 +401,68 @@ if content.endswith("\n"):
|
||||
if normalized != content:
|
||||
path.write_text(normalized, encoding="utf-8")
|
||||
PY
|
||||
done
|
||||
}
|
||||
|
||||
run_instance_start() {
|
||||
local start_output
|
||||
local retry_output
|
||||
local update_output
|
||||
local compose_file
|
||||
local has_redis=0
|
||||
|
||||
compose_file="$(preferred_compose_file)"
|
||||
if [ -f "$compose_file" ] && grep -Eq '^\s{2}redis:\s*$' "$compose_file"; then
|
||||
has_redis=1
|
||||
fi
|
||||
|
||||
stack_is_running() {
|
||||
local running_services
|
||||
running_services="$(docker compose --env-file .docker-build.env ps --status running --services 2>/dev/null || true)"
|
||||
running_services="$(docker compose -f "$compose_file" --env-file .docker-build.env ps --status running --services 2>/dev/null || true)"
|
||||
printf '%s\n' "$running_services" | grep -Fxq app || return 1
|
||||
printf '%s\n' "$running_services" | grep -Fxq nginx || return 1
|
||||
printf '%s\n' "$running_services" | grep -Fxq mongodb || return 1
|
||||
if [ "$has_redis" = "1" ]; then
|
||||
printf '%s\n' "$running_services" | grep -Fxq redis || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$compose_file" = "docker-compose-multitenant.yml" ]; then
|
||||
if start_output="$(docker compose -f "$compose_file" --env-file .docker-build.env up -d --remove-orphans 2>&1)"; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (Inventarsystem Multiinstancing aktiv)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if stack_is_running; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (Multiinstancing läuft, Healthcheck im Startskript war nicht erreichbar)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$start_output" | grep -qi "local app image not found"; then
|
||||
if [ ! -x ./update.sh ]; then
|
||||
fail "Multiinstancing-Start fehlgeschlagen und update.sh fehlt. Letzte Meldung: $(printf '%s' "$start_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
if ! update_output="$(bash ./update.sh 2>&1)"; then
|
||||
if stack_is_running; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (update.sh meldete Healthcheck-Fehler, Multiinstancing-Dienste laufen)."
|
||||
return 0
|
||||
fi
|
||||
fail "update.sh fehlgeschlagen: $(printf '%s' "$update_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
if retry_output="$(docker compose -f "$compose_file" --env-file .docker-build.env up -d --remove-orphans 2>&1)"; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (Multiinstancing-Image automatisch per update.sh geladen)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "Multiinstancing-Start nach update.sh fehlgeschlagen: $(printf '%s' "$retry_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
fail "Multiinstancing-Start fehlgeschlagen: $(printf '%s' "$start_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
if start_output="$(INVENTAR_SETUP_CRON=0 INVENTAR_HTTP_PORT="$HTTP_PORT" INVENTAR_HTTPS_PORT="$HTTPS_PORT" bash ./start.sh --no-cron 2>&1)"; then
|
||||
return 0
|
||||
fi
|
||||
@@ -237,10 +499,11 @@ run_instance_start() {
|
||||
setup_or_update_repo() {
|
||||
local target_dir="$1"
|
||||
local repo_url="$2"
|
||||
local app_image_tag="$3"
|
||||
|
||||
if [ -f "$target_dir/start.sh" ]; then
|
||||
# Existing installation: keep it docker-only and update via project tooling.
|
||||
if [ -x "$target_dir/update.sh" ]; then
|
||||
# Existing installation: keep update flow inside Inventarsystem tooling.
|
||||
if [ "$app_image_tag" = "latest" ] && [ -x "$target_dir/update.sh" ]; then
|
||||
(cd "$target_dir" && bash ./update.sh >/dev/null 2>&1 || true)
|
||||
fi
|
||||
return 0
|
||||
@@ -250,15 +513,7 @@ setup_or_update_repo() {
|
||||
fail "Zielverzeichnis ist nicht leer: $target_dir"
|
||||
fi
|
||||
|
||||
# Clone directly instead of running the upstream installer, which expects
|
||||
# host-level systemd/cron setup and can fail inside containers.
|
||||
if [ -d "$target_dir" ]; then
|
||||
rmdir "$target_dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if ! git clone --depth 1 "$repo_url" "$target_dir" >/dev/null 2>&1; then
|
||||
fail "Repository konnte nicht geklont werden: $repo_url"
|
||||
fi
|
||||
install_from_release "$target_dir" "$repo_url" "$app_image_tag"
|
||||
}
|
||||
|
||||
write_nginx_site() {
|
||||
@@ -453,7 +708,9 @@ FULL_DOMAIN="$SUBDOMAIN.$PARENT_DOMAIN"
|
||||
|
||||
require_cmd docker
|
||||
require_cmd bash
|
||||
require_cmd git
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
require_cmd python3
|
||||
|
||||
mkdir -p "$BASE_DIR"
|
||||
|
||||
@@ -466,10 +723,14 @@ if [ -z "$HTTP_PORT" ]; then
|
||||
fi
|
||||
|
||||
if [ "$ACTION" = "create" ] || [ "$ACTION" = "start" ]; then
|
||||
setup_or_update_repo "$INSTANCE_DIR" "$REPO_URL"
|
||||
setup_or_update_repo "$INSTANCE_DIR" "$REPO_URL" "$APP_IMAGE_TAG"
|
||||
|
||||
[ -f "$INSTANCE_DIR/start.sh" ] || fail "start.sh im Zielrepository nicht gefunden: $INSTANCE_DIR"
|
||||
|
||||
if ! ensure_requested_app_image "$APP_IMAGE_TAG"; then
|
||||
fail "App-Image konnte nicht geladen werden: ghcr.io/aiirondev/legendary-octo-garbanzo:$APP_IMAGE_TAG"
|
||||
fi
|
||||
|
||||
write_env_file "$INSTANCE_DIR" "$HTTP_PORT" "$HTTPS_PORT" "$APP_IMAGE_TAG"
|
||||
set_library_enabled "$INSTANCE_DIR" "$LIBRARY_ENABLED"
|
||||
|
||||
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MONGO_DBPATH="${MONGO_DBPATH:-$SCRIPT_DIR/.mongo-data}"
|
||||
|
||||
start_mongodb_if_needed() {
|
||||
if ! command -v mongod >/dev/null 2>&1; then
|
||||
echo "Error: mongod not found. Install MongoDB first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pgrep -x mongod >/dev/null 2>&1; then
|
||||
echo "MongoDB already running."
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$MONGO_DBPATH"
|
||||
echo "Starting MongoDB with dbPath: $MONGO_DBPATH"
|
||||
mongod --dbpath "$MONGO_DBPATH" --bind_ip 127.0.0.1 --port 27017 --fork --logpath "$SCRIPT_DIR/mongodb.log" || {
|
||||
echo "Failed to start MongoDB. Check $SCRIPT_DIR/mongodb.log for details."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
start_mongodb_if_needed
|
||||
|
||||
# Clean up any existing MongoDB repos to avoid conflicts
|
||||
echo "=== Cleaning up existing MongoDB repositories ==="
|
||||
sudo rm -f /etc/apt/sources.list.d/mongodb*.list
|
||||
sudo apt-key del 7F0CEB10 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 20691EEC35216C63CAF66CE1656408E390CFB1F5 4B7C549A058F8B6B 2069827F925C2E182330D4D4B5BEA7232F5C6971 E162F504A20CDF15827F718D4B7C549A058F8B6B 9DA31620334BD75D9DCB49F368818C72E52529D4 F5679A222C647C87527C2F8CB00A0BD1E2C63C11 2023-02-15 > /dev/null 2>&1 || true
|
||||
# Update system packages
|
||||
echo "=== Updating system packages ==="
|
||||
sudo apt update || { echo "Failed to update package lists"; exit 1; }
|
||||
# Add MongoDB repository depending on OS (Ubuntu Server or Linux Mint)
|
||||
echo "=== Adding MongoDB repository ==="
|
||||
# Detect OS id from /etc/os-release
|
||||
OS_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||||
# Prefer Ubuntu base codename from /etc/os-release when available
|
||||
UBUNTU_BASE_CODENAME=$(awk -F= '/^UBUNTU_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
|
||||
if [ -z "$UBUNTU_BASE_CODENAME" ]; then
|
||||
UBUNTU_BASE_CODENAME=$(lsb_release -cs 2>/dev/null || awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
|
||||
fi
|
||||
if [ "$OS_ID" = "linuxmint" ]; then
|
||||
# Map Linux Mint codename to Ubuntu base codename when needed
|
||||
MINT_CODENAME=$(lsb_release -cs 2>/dev/null || awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
|
||||
if [ -z "$UBUNTU_BASE_CODENAME" ] || [ "$UBUNTU_BASE_CODENAME" = "$MINT_CODENAME" ]; then
|
||||
case "$MINT_CODENAME" in
|
||||
xia) UBUNTU_BASE_CODENAME="noble" ;;
|
||||
vanessa|vera|victoria) UBUNTU_BASE_CODENAME="jammy" ;;
|
||||
ulyana|ulyssa|uma|una) UBUNTU_BASE_CODENAME="focal" ;;
|
||||
esac
|
||||
fi
|
||||
echo "Detected Linux Mint ($MINT_CODENAME) → using Ubuntu base '$UBUNTU_BASE_CODENAME'"
|
||||
elif [ "$OS_ID" = "ubuntu" ];
|
||||
then
|
||||
echo "Detected Ubuntu ($UBUNTU_BASE_CODENAME)"
|
||||
else
|
||||
echo "Non-Ubuntu/Mint OS detected ($OS_ID). Skipping MongoDB apt setup."
|
||||
exit 1
|
||||
fi
|
||||
# Select MongoDB series per Ubuntu base codename
|
||||
case "$UBUNTU_BASE_CODENAME" in
|
||||
noble|jammy)
|
||||
MONGO_SERIES="7.0" ;;
|
||||
focal)
|
||||
MONGO_SERIES="6.0" ;;
|
||||
*)
|
||||
echo "Unknown Ubuntu codename '$UBUNTU_BASE_CODENAME', defaulting to 7.0"
|
||||
MONGO_SERIES="7.0" ;;
|
||||
esac
|
||||
# Use jammy repo path for noble until MongoDB publishes noble (avoid 404)
|
||||
MONGO_APT_CODENAME="$UBUNTU_BASE_CODENAME"
|
||||
if [ "$UBUNTU_BASE_CODENAME" = "noble" ]; then
|
||||
MONGO_APT_CODENAME="jammy"
|
||||
echo "Using jammy repo path for MongoDB on noble"
|
||||
fi
|
||||
# Install repo key and list using series and apt codename
|
||||
wget -qO - https://www.mongodb.org/static/pgp/server-${MONGO_SERIES}.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-${MONGO_SERIES}.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/mongodb-server-${MONGO_SERIES}.gpg arch=amd64,arm64] https://repo.mongodb.org/apt/ubuntu ${MONGO_APT_CODENAME}/mongodb-org/${MONGO_SERIES} multiverse" | \
|
||||
sudo tee /etc/apt/sources.list.d/mongodb-org-${MONGO_SERIES}.list
|
||||
# Install MongoDB
|
||||
sudo apt-get update || exit 1
|
||||
sudo apt-get install -y mongodb-org || exit 1
|
||||
|
||||
if [[ -n "${CONDA_DEFAULT_ENV:-}" ]] && command -v conda >/dev/null 2>&1; then
|
||||
conda deactivate || true
|
||||
fi
|
||||
|
||||
source .venv/bin/activate
|
||||
|
||||
# Do not run apt here because third-party repos in dev containers can fail.
|
||||
# We only check and provide the install hint if patchelf is missing.
|
||||
if ! command -v patchelf >/dev/null 2>&1; then
|
||||
echo "Error: patchelf is required for Nuitka standalone builds on Linux."
|
||||
echo "Install with: sudo apt update && sudo apt install -y patchelf"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install -U nuitka ordered-set zstandard flask flask-jwt-extended cryptography pyotp qrcode bleach pymongo
|
||||
|
||||
python -m nuitka --standalone --follow-imports --include-data-dir=templates=templates --include-data-dir=static=static --include-data-dir=data=data --assume-yes-for-downloads --output-dir=build --remove-output main.py
|
||||
if [[ -x "./build/main.dist/main.bin" ]]; then
|
||||
./build/main.dist/main.bin
|
||||
else
|
||||
echo "Build step finished but executable not found at ./build/main.dist/main.bin"
|
||||
exit 1
|
||||
fi
|
||||
@@ -134,6 +134,18 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel logs-panel">
|
||||
<h2>Live-Logs (Core + Services)</h2>
|
||||
<p class="logs-hint">Zeigt die letzten Zeilen der normalen Dienste direkt im Admin-Frontend an. Aktualisiert automatisch alle 20 Sekunden.</p>
|
||||
<div class="logs-toolbar">
|
||||
<label for="logSourceSelect">Quelle</label>
|
||||
<select id="logSourceSelect"></select>
|
||||
<button type="button" id="refreshLiveLogsBtn">Jetzt aktualisieren</button>
|
||||
<span id="liveLogsUpdatedAt">Stand: -</span>
|
||||
</div>
|
||||
<pre id="liveLogsOutput" class="log-output">Lade Logs ...</pre>
|
||||
</section>
|
||||
|
||||
<section class="instance-tools panel">
|
||||
<h2>Instanz-Operationen</h2>
|
||||
{% if instances %}
|
||||
@@ -357,6 +369,48 @@ button,
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 0.55rem; align-items: center; }
|
||||
.row-actions { gap: 0.4rem; }
|
||||
.actions form { margin: 0; }
|
||||
.logs-panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
.logs-hint {
|
||||
margin-bottom: 0.6rem;
|
||||
color: var(--term-muted);
|
||||
}
|
||||
.logs-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
.logs-toolbar label,
|
||||
.logs-toolbar span {
|
||||
color: var(--term-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.logs-toolbar select {
|
||||
border: 1px solid var(--term-border);
|
||||
border-radius: 8px;
|
||||
background: #0b1114;
|
||||
color: var(--term-text);
|
||||
padding: 0.36rem 0.5rem;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
}
|
||||
.log-output {
|
||||
margin: 0;
|
||||
border: 1px solid var(--term-border);
|
||||
border-radius: 10px;
|
||||
background: #05090b;
|
||||
color: #cbeed0;
|
||||
min-height: 260px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
padding: 0.72rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.inline-admin-form {
|
||||
margin-top: 0.55rem;
|
||||
display: grid;
|
||||
@@ -436,6 +490,10 @@ td {
|
||||
const pct = (value) => `${Number(value || 0).toFixed(1)} %`;
|
||||
const clampPct = (value) => Math.max(0, Math.min(100, Number(value || 0)));
|
||||
const historyLimit = 24;
|
||||
const liveLogsState = {
|
||||
sources: [],
|
||||
selected: ''
|
||||
};
|
||||
|
||||
const history = {
|
||||
labels: [],
|
||||
@@ -605,6 +663,85 @@ td {
|
||||
}
|
||||
};
|
||||
|
||||
const setLiveLogSources = (sources) => {
|
||||
const select = document.getElementById('logSourceSelect');
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = liveLogsState.selected;
|
||||
select.innerHTML = '';
|
||||
|
||||
(sources || []).forEach((source) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = source.key;
|
||||
option.textContent = source.label;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if (!sources || !sources.length) {
|
||||
liveLogsState.sources = [];
|
||||
liveLogsState.selected = '';
|
||||
return;
|
||||
}
|
||||
|
||||
liveLogsState.sources = sources;
|
||||
const hasPrevious = sources.some((item) => item.key === previous);
|
||||
liveLogsState.selected = hasPrevious ? previous : sources[0].key;
|
||||
select.value = liveLogsState.selected;
|
||||
};
|
||||
|
||||
const renderLiveLogs = () => {
|
||||
const output = document.getElementById('liveLogsOutput');
|
||||
const source = liveLogsState.sources.find((item) => item.key === liveLogsState.selected);
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
if (!source) {
|
||||
output.textContent = 'Keine Log-Quelle verfügbar.';
|
||||
return;
|
||||
}
|
||||
output.textContent = source.logs || 'Keine Ausgabe';
|
||||
};
|
||||
|
||||
const updateLiveLogs = async () => {
|
||||
try {
|
||||
const response = await fetch('/admin/system/logs/live', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const sources = [];
|
||||
const core = data.core || {};
|
||||
sources.push({
|
||||
key: 'core',
|
||||
label: `Core Docker (${core.ok ? 'ok' : 'error'})`,
|
||||
logs: core.logs || 'Keine Ausgabe'
|
||||
});
|
||||
|
||||
(data.services || []).forEach((service) => {
|
||||
const name = service.service || 'service';
|
||||
const status = service.status || 'unavailable';
|
||||
sources.push({
|
||||
key: `svc:${name}`,
|
||||
label: `${name} (${status})`,
|
||||
logs: service.logs || 'Keine Ausgabe'
|
||||
});
|
||||
});
|
||||
|
||||
setLiveLogSources(sources);
|
||||
setText('liveLogsUpdatedAt', `Stand: ${data.generated_at || '-'}`);
|
||||
renderLiveLogs();
|
||||
} catch (error) {
|
||||
setText('liveLogsUpdatedAt', 'Stand: Fehler beim Laden');
|
||||
const output = document.getElementById('liveLogsOutput');
|
||||
if (output) {
|
||||
output.textContent = 'Live-Logs konnten nicht geladen werden.';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setUsageBar = (fillId, labelId, valuePct) => {
|
||||
const pctValue = clampPct(valuePct);
|
||||
const fill = document.getElementById(fillId);
|
||||
@@ -679,6 +816,24 @@ td {
|
||||
|
||||
updateSystem();
|
||||
window.setInterval(updateSystem, 15000);
|
||||
|
||||
const logSourceSelect = document.getElementById('logSourceSelect');
|
||||
if (logSourceSelect) {
|
||||
logSourceSelect.addEventListener('change', (event) => {
|
||||
liveLogsState.selected = event.target.value || '';
|
||||
renderLiveLogs();
|
||||
});
|
||||
}
|
||||
|
||||
const refreshLiveLogsBtn = document.getElementById('refreshLiveLogsBtn');
|
||||
if (refreshLiveLogsBtn) {
|
||||
refreshLiveLogsBtn.addEventListener('click', () => {
|
||||
updateLiveLogs();
|
||||
});
|
||||
}
|
||||
|
||||
updateLiveLogs();
|
||||
window.setInterval(updateLiveLogs, 20000);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MONGO_DBPATH="${MONGO_DBPATH:-$SCRIPT_DIR/data/db}"
|
||||
MONGO_LOGPATH="${MONGO_LOGPATH:-$SCRIPT_DIR/data/mongod.log}"
|
||||
|
||||
start_mongodb_if_needed() {
|
||||
if ! command -v mongod >/dev/null 2>&1; then
|
||||
echo "Error: mongod not found. Install MongoDB first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pgrep -x mongod >/dev/null 2>&1; then
|
||||
echo "MongoDB already running."
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$MONGO_DBPATH"
|
||||
echo "Starting MongoDB with dbPath: $MONGO_DBPATH"
|
||||
mongod --dbpath "$MONGO_DBPATH" --bind_ip 127.0.0.1 --port 27017 --fork --logpath "$MONGO_LOGPATH"
|
||||
}
|
||||
|
||||
start_mongodb_if_needed
|
||||
|
||||
@@ -7,7 +7,7 @@ Wants=docker.service network-online.target
|
||||
Type=oneshot
|
||||
User=root
|
||||
WorkingDirectory=/home/max/Dokumente/repos/Key-service-Server
|
||||
ExecStart=/home/max/Dokumente/repos/Key-service-Server/start-stack-on-boot.sh
|
||||
ExecStart=/home/max/Dokumente/repos/Key-service-Server/gitea.sh start
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=0
|
||||
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="create"
|
||||
SCHOOL_NAME=""
|
||||
SUBDOMAIN=""
|
||||
REPO_URL=""
|
||||
BASE_DIR=""
|
||||
PARENT_DOMAIN=""
|
||||
HTTPS_PORT=""
|
||||
HTTP_PORT=""
|
||||
TLS_MODE="development"
|
||||
APP_IMAGE_TAG="latest"
|
||||
LIBRARY_ENABLED="0"
|
||||
WILDCARD_CERT_FILE="/etc/nginx/certs/wildcard.meine-domain.crt"
|
||||
WILDCARD_KEY_FILE="/etc/nginx/certs/wildcard.meine-domain.key"
|
||||
NGINX_SITES_AVAILABLE="/etc/nginx/sites-available"
|
||||
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled"
|
||||
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local message="$1"
|
||||
print_kv "MESSAGE" "$message"
|
||||
print_kv "ERROR" "$message"
|
||||
exit 1
|
||||
}
|
||||
|
||||
slugify() {
|
||||
local input="$1"
|
||||
input="${input,,}"
|
||||
input="$(echo "$input" | sed -E 's/ä/ae/g; s/ö/oe/g; s/ü/ue/g; s/ß/ss/g')"
|
||||
input="$(echo "$input" | sed -E 's/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-+//; s/-+$//')"
|
||||
printf '%s' "${input:0:63}"
|
||||
}
|
||||
|
||||
is_valid_subdomain() {
|
||||
local value="$1"
|
||||
[[ "$value" =~ ^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])$ ]]
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
command -v "$cmd" >/dev/null 2>&1 || fail "Fehlender Befehl: $cmd"
|
||||
}
|
||||
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
|
||||
if docker ps --format '{{.Ports}}' | grep -Eq "(^|[,[:space:]])((0\.0\.0\.0|\[::\]):)?${port}->"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn "( sport = :$port )" 2>/dev/null | awk 'NR>1 {print $4}' | grep -q . && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
next_free_port() {
|
||||
local start_port="$1"
|
||||
local current="$start_port"
|
||||
while is_port_in_use "$current"; do
|
||||
current=$((current + 1))
|
||||
done
|
||||
printf '%s' "$current"
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local target_dir="$1"
|
||||
local http_port="$2"
|
||||
local https_port="$3"
|
||||
local app_image_tag="$4"
|
||||
|
||||
cat > "$target_dir/.docker-build.env" <<EOF
|
||||
NUITKA_BUILD=0
|
||||
INVENTAR_HTTP_PORT=$http_port
|
||||
INVENTAR_HTTPS_PORT=$https_port
|
||||
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$app_image_tag
|
||||
EOF
|
||||
}
|
||||
|
||||
set_library_enabled() {
|
||||
local instance_dir="$1"
|
||||
local enabled="$2"
|
||||
local cfg="$instance_dir/config.json"
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
python3 - <<'PY' "$cfg" "$enabled"
|
||||
import json
|
||||
import sys
|
||||
|
||||
cfg_path = sys.argv[1]
|
||||
enabled = sys.argv[2] == "1"
|
||||
|
||||
with open(cfg_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
modules = data.get("modules")
|
||||
if not isinstance(modules, dict):
|
||||
modules = {}
|
||||
data["modules"] = modules
|
||||
|
||||
library = modules.get("library")
|
||||
if not isinstance(library, dict):
|
||||
library = {}
|
||||
modules["library"] = library
|
||||
|
||||
library["enabled"] = enabled
|
||||
|
||||
with open(cfg_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
PY
|
||||
}
|
||||
|
||||
normalize_instance_compose() {
|
||||
local compose_file="docker-compose.yml"
|
||||
[ -f "$compose_file" ] || return 0
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("docker-compose.yml")
|
||||
content = path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
|
||||
out = []
|
||||
in_nginx = False
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
indent = len(line) - len(line.lstrip(" "))
|
||||
|
||||
if stripped.startswith("nginx:") and indent == 2:
|
||||
in_nginx = True
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_nginx and indent == 2 and stripped.endswith(":") and not stripped.startswith("nginx:"):
|
||||
in_nginx = False
|
||||
|
||||
if stripped.startswith("container_name:") and indent == 4:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_nginx and stripped.startswith("ports:") and indent == 4:
|
||||
out.append(line)
|
||||
out.append(" - \"${INVENTAR_HTTP_PORT:-80}:80\"")
|
||||
out.append(" - \"${INVENTAR_HTTPS_PORT:-443}:443\"")
|
||||
i += 1
|
||||
while i < len(lines):
|
||||
nxt = lines[i]
|
||||
nxt_stripped = nxt.strip()
|
||||
nxt_indent = len(nxt) - len(nxt.lstrip(" "))
|
||||
if nxt_stripped.startswith("-") and nxt_indent >= 6:
|
||||
i += 1
|
||||
continue
|
||||
break
|
||||
continue
|
||||
|
||||
out.append(line)
|
||||
i += 1
|
||||
|
||||
normalized = "\n".join(out)
|
||||
if content.endswith("\n"):
|
||||
normalized += "\n"
|
||||
|
||||
if normalized != content:
|
||||
path.write_text(normalized, encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
run_instance_start() {
|
||||
local start_output
|
||||
local retry_output
|
||||
local update_output
|
||||
|
||||
stack_is_running() {
|
||||
local running_services
|
||||
running_services="$(docker compose --env-file .docker-build.env ps --status running --services 2>/dev/null || true)"
|
||||
printf '%s\n' "$running_services" | grep -Fxq app || return 1
|
||||
printf '%s\n' "$running_services" | grep -Fxq nginx || return 1
|
||||
printf '%s\n' "$running_services" | grep -Fxq mongodb || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
if start_output="$(INVENTAR_SETUP_CRON=0 INVENTAR_HTTP_PORT="$HTTP_PORT" INVENTAR_HTTPS_PORT="$HTTPS_PORT" bash ./start.sh --no-cron 2>&1)"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if stack_is_running; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (Healthcheck im Startskript war nicht erreichbar, Dienste laufen)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$start_output" | grep -qi "local app image not found"; then
|
||||
if [ ! -x ./update.sh ]; then
|
||||
fail "start.sh fehlgeschlagen und update.sh fehlt. Letzte Meldung: $(printf '%s' "$start_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
if ! update_output="$(bash ./update.sh 2>&1)"; then
|
||||
if stack_is_running; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (update.sh meldete Healthcheck-Fehler, Dienste laufen)."
|
||||
return 0
|
||||
fi
|
||||
fail "update.sh fehlgeschlagen: $(printf '%s' "$update_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
if retry_output="$(INVENTAR_SETUP_CRON=0 INVENTAR_HTTP_PORT="$HTTP_PORT" INVENTAR_HTTPS_PORT="$HTTPS_PORT" bash ./start.sh --no-cron 2>&1)"; then
|
||||
print_kv "MESSAGE" "Instanz gestartet (Image automatisch per update.sh geladen)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "Start nach update.sh fehlgeschlagen: $(printf '%s' "$retry_output" | tail -n1)"
|
||||
fi
|
||||
|
||||
fail "start.sh fehlgeschlagen: $(printf '%s' "$start_output" | tail -n1)"
|
||||
}
|
||||
|
||||
setup_or_update_repo() {
|
||||
local target_dir="$1"
|
||||
local repo_url="$2"
|
||||
|
||||
if [ -f "$target_dir/start.sh" ]; then
|
||||
# Existing installation: keep it docker-only and update via project tooling.
|
||||
if [ -x "$target_dir/update.sh" ]; then
|
||||
(cd "$target_dir" && bash ./update.sh >/dev/null 2>&1 || true)
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -d "$target_dir" ] && [ "$(find "$target_dir" -mindepth 1 -maxdepth 1 | wc -l)" -gt 0 ]; then
|
||||
fail "Zielverzeichnis ist nicht leer: $target_dir"
|
||||
fi
|
||||
|
||||
# Clone directly instead of running the upstream installer, which expects
|
||||
# host-level systemd/cron setup and can fail inside containers.
|
||||
if [ -d "$target_dir" ]; then
|
||||
rmdir "$target_dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if ! git clone --depth 1 "$repo_url" "$target_dir" >/dev/null 2>&1; then
|
||||
fail "Repository konnte nicht geklont werden: $repo_url"
|
||||
fi
|
||||
}
|
||||
|
||||
write_nginx_site() {
|
||||
local subdomain="$1"
|
||||
local full_domain="$2"
|
||||
local https_port="$3"
|
||||
local cert_file=""
|
||||
local key_file=""
|
||||
local cert_dir="/etc/nginx/certs"
|
||||
local site_name="inventarsystem-${subdomain}.conf"
|
||||
local avail_file="$NGINX_SITES_AVAILABLE/$site_name"
|
||||
local enabled_file="$NGINX_SITES_ENABLED/$site_name"
|
||||
|
||||
if [ ! -d "$NGINX_SITES_AVAILABLE" ] || [ ! -d "$NGINX_SITES_ENABLED" ]; then
|
||||
print_kv "NGINX_STATUS" "skipped"
|
||||
print_kv "MESSAGE" "Instanz gestartet. Nginx-Verzeichnisse nicht gefunden, Reverse-Proxy manuell anlegen."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -w "$NGINX_SITES_AVAILABLE" ] || [ ! -w "$NGINX_SITES_ENABLED" ]; then
|
||||
print_kv "NGINX_STATUS" "manual_required"
|
||||
print_kv "MESSAGE" "Instanz gestartet. Keine Schreibrechte auf Nginx-Konfiguration, bitte als root/sudo ausführen."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$TLS_MODE" = "production" ]; then
|
||||
cert_file="$WILDCARD_CERT_FILE"
|
||||
key_file="$WILDCARD_KEY_FILE"
|
||||
|
||||
if [ ! -f "$cert_file" ] || [ ! -f "$key_file" ]; then
|
||||
print_kv "NGINX_STATUS" "error"
|
||||
print_kv "MESSAGE" "Produktivmodus aktiv, aber Wildcard-Zertifikat fehlt: $cert_file / $key_file"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
cert_file="$cert_dir/$full_domain.crt"
|
||||
key_file="$cert_dir/$full_domain.key"
|
||||
|
||||
if [ ! -f "$cert_file" ] || [ ! -f "$key_file" ]; then
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
print_kv "NGINX_STATUS" "manual_required"
|
||||
print_kv "MESSAGE" "Instanz gestartet. openssl fehlt für Development-Zertifikat, Zertifikat bitte manuell anlegen."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -d "$cert_dir" ] || [ ! -w "$cert_dir" ]; then
|
||||
print_kv "NGINX_STATUS" "manual_required"
|
||||
print_kv "MESSAGE" "Instanz gestartet. Keine Schreibrechte auf $cert_dir für Development-Zertifikat."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! openssl req -x509 -nodes -newkey rsa:2048 -sha256 \
|
||||
-days 825 \
|
||||
-keyout "$key_file" \
|
||||
-out "$cert_file" \
|
||||
-subj "/CN=$full_domain" \
|
||||
-addext "subjectAltName=DNS:$full_domain" >/dev/null 2>&1; then
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -sha256 \
|
||||
-days 825 \
|
||||
-keyout "$key_file" \
|
||||
-out "$cert_file" \
|
||||
-subj "/CN=$full_domain" >/dev/null 2>&1 || {
|
||||
print_kv "NGINX_STATUS" "manual_required"
|
||||
print_kv "MESSAGE" "Instanz gestartet. Development-Zertifikat konnte nicht erstellt werden."
|
||||
return 0
|
||||
}
|
||||
fi
|
||||
|
||||
chmod 600 "$key_file" 2>/dev/null || true
|
||||
chmod 644 "$cert_file" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$avail_file" <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $full_domain;
|
||||
return 301 https://\$host\$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name $full_domain;
|
||||
|
||||
ssl_certificate $cert_file;
|
||||
ssl_certificate_key $key_file;
|
||||
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:$https_port;
|
||||
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;
|
||||
proxy_ssl_verify off;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
ln -sfn "$avail_file" "$enabled_file"
|
||||
|
||||
if ! command -v nginx >/dev/null 2>&1; then
|
||||
print_kv "NGINX_STATUS" "manual_required"
|
||||
print_kv "MESSAGE" "Instanz gestartet. Nginx-Site wurde geschrieben, Reload bitte auf dem Host ausführen."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if nginx -t >/dev/null 2>&1; then
|
||||
nginx -s reload >/dev/null 2>&1 || true
|
||||
print_kv "NGINX_STATUS" "ok"
|
||||
else
|
||||
print_kv "NGINX_STATUS" "error"
|
||||
print_kv "MESSAGE" "Instanz gestartet, aber nginx -t ist fehlgeschlagen."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--action)
|
||||
ACTION="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--school-name)
|
||||
SCHOOL_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--subdomain)
|
||||
SUBDOMAIN="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
REPO_URL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--base-dir)
|
||||
BASE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--domain)
|
||||
PARENT_DOMAIN="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--https-port)
|
||||
HTTPS_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--http-port)
|
||||
HTTP_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--tls-mode)
|
||||
TLS_MODE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--wildcard-cert-file)
|
||||
WILDCARD_CERT_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--wildcard-key-file)
|
||||
WILDCARD_KEY_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--app-image-tag)
|
||||
APP_IMAGE_TAG="${2:-latest}"
|
||||
shift 2
|
||||
;;
|
||||
--library-enabled)
|
||||
LIBRARY_ENABLED="${2:-0}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
fail "Unbekannter Parameter: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$REPO_URL" ] || fail "Repository-URL fehlt"
|
||||
[ -n "$BASE_DIR" ] || fail "Basisverzeichnis fehlt"
|
||||
[ -n "$PARENT_DOMAIN" ] || fail "Parent-Domain fehlt"
|
||||
|
||||
if [ -z "$SUBDOMAIN" ]; then
|
||||
SUBDOMAIN="$(slugify "$SCHOOL_NAME")"
|
||||
else
|
||||
SUBDOMAIN="$(slugify "$SUBDOMAIN")"
|
||||
fi
|
||||
|
||||
is_valid_subdomain "$SUBDOMAIN" || fail "Ungueltige Subdomain: $SUBDOMAIN"
|
||||
|
||||
INSTANCE_DIR="$BASE_DIR/$SUBDOMAIN"
|
||||
FULL_DOMAIN="$SUBDOMAIN.$PARENT_DOMAIN"
|
||||
|
||||
require_cmd docker
|
||||
require_cmd bash
|
||||
require_cmd git
|
||||
|
||||
mkdir -p "$BASE_DIR"
|
||||
|
||||
if [ -z "$HTTPS_PORT" ]; then
|
||||
HTTPS_PORT="$(next_free_port 15443)"
|
||||
fi
|
||||
|
||||
if [ -z "$HTTP_PORT" ]; then
|
||||
HTTP_PORT="$(next_free_port 15080)"
|
||||
fi
|
||||
|
||||
if [ "$ACTION" = "create" ] || [ "$ACTION" = "start" ]; then
|
||||
setup_or_update_repo "$INSTANCE_DIR" "$REPO_URL"
|
||||
|
||||
[ -f "$INSTANCE_DIR/start.sh" ] || fail "start.sh im Zielrepository nicht gefunden: $INSTANCE_DIR"
|
||||
|
||||
write_env_file "$INSTANCE_DIR" "$HTTP_PORT" "$HTTPS_PORT" "$APP_IMAGE_TAG"
|
||||
set_library_enabled "$INSTANCE_DIR" "$LIBRARY_ENABLED"
|
||||
|
||||
cd "$INSTANCE_DIR"
|
||||
normalize_instance_compose
|
||||
run_instance_start
|
||||
|
||||
write_nginx_site "$SUBDOMAIN" "$FULL_DOMAIN" "$HTTPS_PORT"
|
||||
|
||||
print_kv "SUBDOMAIN" "$SUBDOMAIN"
|
||||
print_kv "DOMAIN" "$FULL_DOMAIN"
|
||||
print_kv "HTTPS_PORT" "$HTTPS_PORT"
|
||||
print_kv "HTTP_PORT" "$HTTP_PORT"
|
||||
print_kv "APP_IMAGE_TAG" "$APP_IMAGE_TAG"
|
||||
print_kv "LIBRARY_ENABLED" "$LIBRARY_ENABLED"
|
||||
print_kv "INSTANCE_DIR" "$INSTANCE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fail "Nicht unterstuetzte Action: $ACTION"
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
RUN_START=1
|
||||
RUN_NGINX=1
|
||||
RUN_HOSTS_SYNC=1
|
||||
RUN_AUTOSTART=1
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: sudo ./setup-first-install.sh [options]
|
||||
|
||||
Options:
|
||||
--skip-start Skip stack start via gitea.sh
|
||||
--skip-nginx Skip nginx apply via nginx.sh
|
||||
--skip-hosts-sync Skip installation of invario-hosts-sync.service
|
||||
--skip-autostart Skip installation of invario-stack-autostart.service
|
||||
-h, --help Show this help
|
||||
|
||||
Default behavior:
|
||||
1) Start stack
|
||||
2) Apply nginx
|
||||
3) Install hosts-sync service
|
||||
4) Install stack-autostart service
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-start)
|
||||
RUN_START=0
|
||||
shift
|
||||
;;
|
||||
--skip-nginx)
|
||||
RUN_NGINX=0
|
||||
shift
|
||||
;;
|
||||
--skip-hosts-sync)
|
||||
RUN_HOSTS_SYNC=0
|
||||
shift
|
||||
;;
|
||||
--skip-autostart)
|
||||
RUN_AUTOSTART=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "This script must be run as root. Use: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_script() {
|
||||
local file="$1"
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "Missing required file: $file"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$file" ]]; then
|
||||
chmod +x "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
require_script "$SCRIPT_DIR/gitea.sh"
|
||||
require_script "$SCRIPT_DIR/nginx.sh"
|
||||
require_script "$SCRIPT_DIR/setup-hosts-sync.sh"
|
||||
require_script "$SCRIPT_DIR/setup-stack-autostart.sh"
|
||||
|
||||
echo "Running first installation setup in: $SCRIPT_DIR"
|
||||
|
||||
action_or_skip() {
|
||||
local flag="$1"
|
||||
local title="$2"
|
||||
shift 2
|
||||
|
||||
if [[ "$flag" -eq 1 ]]; then
|
||||
echo "[RUN ] $title"
|
||||
"$@"
|
||||
else
|
||||
echo "[SKIP] $title"
|
||||
fi
|
||||
}
|
||||
|
||||
action_or_skip "$RUN_START" "Start stack (gitea.sh start)" "$SCRIPT_DIR/gitea.sh" start
|
||||
action_or_skip "$RUN_NGINX" "Apply nginx (nginx.sh apply)" "$SCRIPT_DIR/nginx.sh" apply
|
||||
action_or_skip "$RUN_HOSTS_SYNC" "Install hosts-sync service" "$SCRIPT_DIR/setup-hosts-sync.sh"
|
||||
action_or_skip "$RUN_AUTOSTART" "Install stack-autostart service" "$SCRIPT_DIR/setup-stack-autostart.sh"
|
||||
|
||||
echo
|
||||
echo "First installation setup completed."
|
||||
echo "Service status checks:"
|
||||
echo " systemctl status invario-hosts-sync"
|
||||
echo " systemctl status invario-stack-autostart"
|
||||
echo "Stack status:"
|
||||
echo " $SCRIPT_DIR/gitea.sh status"
|
||||
@@ -5,18 +5,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICE_NAME="invario-stack-autostart"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
SERVICE_TEMPLATE="$SCRIPT_DIR/${SERVICE_NAME}.service"
|
||||
BOOT_SCRIPT="$SCRIPT_DIR/start-stack-on-boot.sh"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "This script must be run as root. Use: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$BOOT_SCRIPT" ]]; then
|
||||
echo "Missing executable boot script: $BOOT_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SERVICE_TEMPLATE" ]]; then
|
||||
echo "Missing service template: $SERVICE_TEMPLATE"
|
||||
exit 1
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Start the full stack and refresh routing on every boot.
|
||||
./gitea.sh start
|
||||
Reference in New Issue
Block a user