102 lines
5.5 KiB
Python
102 lines
5.5 KiB
Python
"""Local management via fixed SSH target or container-private Unix RPC."""
|
|
from auth import initial_record
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import secrets
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SSH = ['ssh', '-i', str(Path.home()/'.ssh/athena_key'), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=yes', 'root@192.168.1.212']
|
|
|
|
class NetworkClient:
|
|
def __init__(self):
|
|
self.disabled = os.environ.get('DECK_NETWORK_MODE') == 'disabled'
|
|
self.local = os.environ.get('DECK_NETWORK_SOCKET') == '1'
|
|
self.job = None
|
|
self.lock = threading.Lock()
|
|
self.cached = None
|
|
self.cached_at = 0
|
|
|
|
def call(self, action, values=None, ingress='management'):
|
|
if self.disabled:
|
|
raise ValueError('WireGuard ist in dieser eigenständigen Installation nicht angebunden.')
|
|
data = dict(values or {}, action=action)
|
|
if self.local:
|
|
from network.rpc import request
|
|
data.update(ingress=ingress, proxy_token=os.environ.get('DECK_PROXY_TOKEN', ''))
|
|
return request(data)
|
|
result = subprocess.run(SSH + ['docker exec -i athena-deck-network python3 -m network.rpc'], input=json.dumps(data), text=True, capture_output=True, timeout=35)
|
|
try:
|
|
value = json.loads(result.stdout)
|
|
except ValueError:
|
|
raise ValueError('Deck-Netzwerkmodul auf Athena nicht erreichbar oder noch nicht installiert.') from None
|
|
if result.returncode or ('error' in value and 'installed' not in value):
|
|
raise ValueError(value.get('error', 'Netzwerkaktion fehlgeschlagen.'))
|
|
self.cached = None
|
|
return value
|
|
|
|
def status(self, ingress='management'):
|
|
if self.disabled:
|
|
return dict(installed=False, install_supported=False, state='disabled-module', ingress=ingress, error='Eigenständige Installation ohne WireGuard-Modul. Zugriff ist per SSH-Tunnel möglich. Vorhandene Gateways werden nicht verändert.')
|
|
with self.lock:
|
|
if self.job and self.job['state'] == 'running':
|
|
return dict(installed=False, state='installing', job=self.job.copy(), ingress=ingress)
|
|
if self.cached is not None and time.monotonic()-self.cached_at < 3:
|
|
return dict(self.cached, ingress=ingress, job=self.job)
|
|
try:
|
|
result = self.call('status', ingress=ingress)
|
|
result['reachable'] = True
|
|
except (ValueError, OSError, subprocess.SubprocessError):
|
|
result = dict(installed=False, reachable=False, state='unavailable', error='Netzwerkmodul noch nicht installiert oder Athena nicht erreichbar.')
|
|
self.cached = result
|
|
self.cached_at = time.monotonic()
|
|
return dict(result, ingress=ingress, job=self.job)
|
|
|
|
def install(self, password, api_token=None):
|
|
if self.disabled:
|
|
raise ValueError('WireGuard-Installation ist für diese eigenständige Instanz deaktiviert.')
|
|
if self.local:
|
|
raise ValueError('Die Server-Instanz ist bereits installiert.')
|
|
if not isinstance(password, str) or not 16 <= len(password) <= 256:
|
|
raise ValueError('Bitte ein eigenes Deck-Passwort mit mindestens 16 Zeichen setzen.')
|
|
auth = initial_record(password,api_token)
|
|
with self.lock:
|
|
if self.job and self.job['state'] == 'running':
|
|
raise ValueError('Installation läuft bereits.')
|
|
self.job = dict(state='running', message='Eigener Deck-Container wird gebaut und gestartet. Das kann mehrere Minuten dauern.')
|
|
threading.Thread(target=self._install, args=(auth,), daemon=True).start()
|
|
return dict(job=self.job.copy())
|
|
|
|
def _install(self, auth):
|
|
try:
|
|
# Import the source allowlist without executing the installer entrypoint.
|
|
from network.install_remote import FILES
|
|
payload = dict(auth=auth, files={name:base64.b64encode((ROOT/name).read_bytes()).decode() for name in FILES})
|
|
script = (ROOT/'network/install_remote.py').read_text()
|
|
# Remote command contains only fixed trusted program text, never user input.
|
|
import shlex
|
|
command = 'python3 -c ' + shlex.quote(script)
|
|
result = subprocess.run(SSH+[command], input=json.dumps(payload), text=True, capture_output=True, timeout=900)
|
|
value = json.loads(result.stdout)
|
|
if result.returncode or ('error' in value and 'installed' not in value):
|
|
raise ValueError(value.get('error', 'Installation fehlgeschlagen.'))
|
|
# Confirm helper readiness; don't report a successful installation from docker run alone.
|
|
for _ in range(20):
|
|
try:
|
|
self.call('status')
|
|
break
|
|
except (ValueError, OSError, subprocess.SubprocessError):
|
|
time.sleep(1)
|
|
else:
|
|
raise ValueError('Container angelegt, aber Netzwerk-Helper nicht bereit. Installation prüfen.')
|
|
self.job = dict(state='complete', message='Netzwerkmodul installiert. Jetzt eine eigene WireGuard-Konfiguration importieren.')
|
|
except Exception as exc:
|
|
self.job = dict(state='failed', message=str(exc) if isinstance(exc, ValueError) and not isinstance(exc, json.JSONDecodeError) else 'Installation fehlgeschlagen; SSH und Docker prüfen.')
|
|
finally:
|
|
self.cached = None
|