Add encrypted configuration backup and planned restore
This commit is contained in:
+4
-2
@@ -11,11 +11,13 @@ RUN git clone https://github.com/Comfy-Org/ComfyUI.git /opt/deck-comfy \
|
||||
&& /opt/deck-image-python/bin/pip install --no-cache-dir -c /tmp/image-requirements.lock -r /opt/deck-comfy/requirements.txt -r /opt/deck-comfy/custom_nodes/ComfyUI-GGUF/requirements.txt accelerate==1.15.0 \
|
||||
&& /opt/deck-image-python/bin/pip freeze > /opt/deck-comfy/deck-requirements.lock
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3-dev && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock
|
||||
COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock
|
||||
COPY audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/
|
||||
COPY video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/
|
||||
COPY backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/
|
||||
COPY backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/
|
||||
COPY deploy/docker_backup.py /app/deploy/docker_backup.py
|
||||
COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \
|
||||
DECK_BIND_HOST=0.0.0.0 DECK_STATE_DIR=/var/lib/deck \
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
ARG DECK_RUNTIME_IMAGE
|
||||
FROM ${DECK_RUNTIME_IMAGE}
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock
|
||||
COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock
|
||||
COPY audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/
|
||||
COPY video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/
|
||||
COPY backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/
|
||||
COPY backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/
|
||||
COPY deploy/docker_backup.py /app/deploy/docker_backup.py
|
||||
COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \
|
||||
DECK_BIND_HOST=0.0.0.0 DECK_STATE_DIR=/var/lib/deck \
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Configuration-only backup of labelled apps; no Docker socket exposed to the GUI.
|
||||
Imported containers never get privileged mode, devices, Docker sockets or host filesystem mounts.
|
||||
"""
|
||||
import base64,json,os,re,shutil,subprocess,socket,time
|
||||
from pathlib import Path,PurePosixPath
|
||||
MAX_FILE=1024*1024
|
||||
APP='io.athena-deck.application'
|
||||
LABELS={'io.athena-deck.managed':'true','io.athena-deck.role':'application'}
|
||||
|
||||
def command(args,timeout=60):
|
||||
r=subprocess.run(['/usr/bin/docker',*args],capture_output=True,text=True,timeout=timeout)
|
||||
if r.returncode:raise ValueError('Docker-Schritt fehlgeschlagen; Image-Zugang, Port und Docker prüfen.')
|
||||
return r.stdout.strip()
|
||||
|
||||
def blob(path):
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size>MAX_FILE:raise ValueError('Dienstkonfiguration nicht sicher lesbar oder größer als 1 MiB.')
|
||||
return base64.b64encode(path.read_bytes()).decode()
|
||||
|
||||
def config_files(source,destination):
|
||||
p=Path(source)
|
||||
if destination.startswith('/run/secrets/'):
|
||||
return {'file':blob(p)} if p.exists() else {}
|
||||
if destination.endswith('/Data') and p.is_dir():
|
||||
files={}
|
||||
for name in ('Settings.fds','Backends.fds'):
|
||||
if (p/name).is_file():files[name]=blob(p/name)
|
||||
return files
|
||||
return {}
|
||||
|
||||
def app_uid(container,user):
|
||||
if not user:return 0
|
||||
if user.split(':')[0].isdigit():return int(user.split(':')[0])
|
||||
with __import__('tempfile').TemporaryDirectory() as d:
|
||||
p=Path(d)/'passwd';command(['cp',container+':/etc/passwd',str(p)])
|
||||
row=next((x.split(':') for x in p.read_text().splitlines() if x.split(':')[0]==user.split(':')[0]),None)
|
||||
if not row:raise ValueError('Containerbenutzer nicht auflösbar.')
|
||||
return int(row[2])
|
||||
|
||||
def export(manager):
|
||||
policy=manager.state/'backup-policy.json'
|
||||
deck=json.loads(policy.read_text())['deck_state'] if policy.exists() else None
|
||||
result=[]
|
||||
for row in manager.inventory():
|
||||
x=json.loads(command(['inspect',row['id']]))[0];c=x['Config'];h=x['HostConfig'];image=json.loads(command(['image','inspect',x['Image']]))[0]
|
||||
registry=next((d for d in image.get('RepoDigests',[]) if '/' in d.split('@')[0] and ('.' in d.split('/')[0] or ':' in d.split('/')[0])),None)
|
||||
if not registry and row['name']!='athena-swarm-ui':registry=next(iter(image.get('RepoDigests',[])),None)
|
||||
mounts=[]
|
||||
for i,m in enumerate(x['Mounts']):
|
||||
source=m['Source'];relative=None
|
||||
if deck:
|
||||
try:relative=str(Path(source).relative_to(deck))
|
||||
except ValueError:pass
|
||||
mounts.append(dict(index=i,target=m['Destination'],read_only=not m['RW'],deck_path=relative,files=config_files(source,m['Destination']),was_file=Path(source).is_file()))
|
||||
result.append(dict(name=row['name'],image=c['Image'],registry=registry,application=c.get('Labels',{}).get(APP,''),labels={k:v for k,v in (c.get('Labels') or {}).items() if k.startswith('io.athena-deck.')},env=c.get('Env') or [],entrypoint=c.get('Entrypoint'),cmd=c.get('Cmd'),uid=app_uid(row['id'],c.get('User') or ''),user=c.get('User') or '',workdir=c.get('WorkingDir') or '',network=h['NetworkMode'],ports=h.get('PortBindings') or {},restart=h.get('RestartPolicy',{}).get('Name') or 'no',memory=h.get('Memory') or 0,nanocpus=h.get('NanoCpus') or 0,running=x['State']['Running'],mounts=mounts,build_recipe='swarm-ui' if row['name']=='athena-swarm-ui' else None,unsupported=bool(h.get('Privileged') or h.get('DeviceRequests') or h.get('Devices') or h.get('CapAdd'))))
|
||||
return {'services':result,'configured':bool(deck)}
|
||||
|
||||
def validate(c):
|
||||
if not isinstance(c,dict) or not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}',c.get('name','')):raise ValueError('Ungültiger Containername.')
|
||||
if any(c.get('labels',{}).get(k)!=v for k,v in LABELS.items()):raise ValueError('Nur ausdrücklich Deck zugeordnete Anwendungscontainer erlaubt.')
|
||||
if c.get('unsupported'):raise ValueError('GPU-/privilegierter Container benötigt manuelle Einrichtung.')
|
||||
if c.get('network') not in ('bridge','default','host'):raise ValueError('Benutzerdefiniertes Docker-Netzwerk benötigt manuelle Einrichtung.')
|
||||
if c.get('network')=='host' and c['name'] not in ('athena-swarm-ui','ltx-deskweb'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.')
|
||||
if c.get('build_recipe') not in (None,'swarm-ui') or c.get('build_recipe')=='swarm-ui' and (c['name']!='athena-swarm-ui' or c.get('image')!='athena-swarm-ui:de7b834'):raise ValueError('Unbekanntes Build-Rezept.')
|
||||
ref=c.get('registry') or c.get('image','')
|
||||
if not isinstance(ref,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/@:-]{0,300}',ref):raise ValueError('Ungültige Image-Referenz.')
|
||||
if c.get('restart') not in ('no','always','unless-stopped','on-failure'):raise ValueError('Ungültige Neustartregel.')
|
||||
for field in ('env','entrypoint','cmd'):
|
||||
v=c.get(field)
|
||||
if v is not None and (not isinstance(v,list) or len(v)>200 or any(not isinstance(t,str) or len(t)>16384 or '\x00' in t for t in v)):raise ValueError('Ungültige Containerparameter.')
|
||||
for field in ('user','workdir'):
|
||||
if not isinstance(c.get(field,''),str) or len(c.get(field,''))>512 or '\x00' in c.get(field,''):raise ValueError('Ungültige Containerparameter.')
|
||||
if type(c.get('uid',0)) is not int or not 0<=c.get('uid',0)<=4294967294:raise ValueError('Ungültige Anwendungs-UID.')
|
||||
if type(c.get('running')) is not bool or not isinstance(c.get('labels'),dict) or len(c['labels'])>30 or any(not isinstance(k,str) or not k.startswith('io.athena-deck.') or not isinstance(v,str) or len(v)>512 for k,v in c['labels'].items()):raise ValueError('Ungültige Container-Einstellungen.')
|
||||
for field in ('memory','nanocpus'):
|
||||
if type(c.get(field)) is not int or not 0<=c[field]<=1024**5:raise ValueError('Ungültige Ressourcenbegrenzung.')
|
||||
ports=c.get('ports')
|
||||
if not isinstance(ports,dict) or len(ports)>20:raise ValueError('Ungültige Ports.')
|
||||
for container,bindings in ports.items():
|
||||
if not re.fullmatch(r'\d{1,5}/(?:tcp|udp)',container) or not 1<=int(container.split('/')[0])<=65535 or not isinstance(bindings,list):raise ValueError('Ungültige Ports.')
|
||||
for binding in bindings:
|
||||
if binding.get('HostIp') not in ('127.0.0.1','::1'):raise ValueError('Restore veröffentlicht Containerports nur auf Loopback.')
|
||||
if not str(binding.get('HostPort','')).isdigit() or not 1024<=int(binding['HostPort'])<=65535:raise ValueError('Ungültiger Host-Port.')
|
||||
mounts=c.get('mounts')
|
||||
if not isinstance(mounts,list) or len(mounts)>30:raise ValueError('Ungültige Volumes.')
|
||||
targets=set()
|
||||
for i,m in enumerate(mounts):
|
||||
target=m.get('target','');parts=PurePosixPath(target).parts
|
||||
if not target.startswith('/') or '..' in parts or ',' in target or ':' in target or target in targets or target.startswith(('/proc','/sys','/dev','/etc','/var/run','/run/athena')) or 'docker.sock' in target:raise ValueError('Unsicheres Volume-Ziel.')
|
||||
targets.add(target)
|
||||
if m.get('index')!=i or type(m.get('read_only')) is not bool or type(m.get('was_file')) is not bool:raise ValueError('Ungültige Volume-Einstellung.')
|
||||
rel=m.get('deck_path')
|
||||
if rel is not None and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Dateien dürfen nur als bekannte, lesende Modell-/Token-Volumes eingebunden werden.')
|
||||
for name,data in m.get('files',{}).items():
|
||||
if name not in ('file','Settings.fds','Backends.fds'):raise ValueError('Unbekannte Konfigurationsdatei.')
|
||||
if not isinstance(data,str) or len(base64.b64decode(data,validate=True))>MAX_FILE:raise ValueError('Ungültige Konfigurationsdatei.')
|
||||
return c
|
||||
|
||||
def restore(manager,c):
|
||||
validate(c)
|
||||
policy=manager.state/'backup-policy.json'
|
||||
if not policy.is_file():raise ValueError('Deck-Zustandsverzeichnis muss im Systemhelfer registriert sein.')
|
||||
if policy.is_symlink() or policy.stat().st_uid!=0 or policy.stat().st_mode&0o022:raise ValueError('Systemhelfer-Registrierung nicht vertrauenswürdig.')
|
||||
deck=Path(json.loads(policy.read_text())['deck_state']).resolve();name=c['name']
|
||||
existing=subprocess.run(['/usr/bin/docker','inspect',name],capture_output=True,text=True)
|
||||
if existing.returncode==0:
|
||||
x=json.loads(existing.stdout)[0]
|
||||
if any(x['Config'].get('Labels',{}).get(k)!=v for k,v in LABELS.items()):raise ValueError('Containername ist durch einen fremden Dienst belegt.')
|
||||
# Never replace or restart an existing service during a restore.
|
||||
if x['Config']['Image'] not in (c['image'],c.get('registry')):raise ValueError('Vorhandener Container verwendet ein anderes Image; manuell prüfen.')
|
||||
return {'state':'reused','message':'Vorhandener markierter Container unverändert übernommen.'}
|
||||
if c.get('build_recipe')=='swarm-ui':
|
||||
source=Path(__file__).parent/'swarm-ui'
|
||||
if not (source/'Dockerfile').is_file():raise ValueError('Gepinntes Swarm-Build-Rezept fehlt im Systemhelfer.')
|
||||
command(['build','-t',c['image'],str(source)],1800)
|
||||
else:
|
||||
if not c.get('registry'):raise ValueError('Lokales Image hat keine Registry-Quelle und kein bekanntes Build-Rezept.')
|
||||
command(['pull',c['registry']],1800)
|
||||
if c.get('build_recipe')=='swarm-ui':
|
||||
nodes=deck/'video/swarm-comfy-nodes';nodes.mkdir(parents=True,exist_ok=True)
|
||||
temporary=command(['create',c['image']])
|
||||
try:command(['cp',temporary+':/swarm/src/BuiltinExtensions/ComfyUIBackend/ExtraNodes/.',str(nodes)])
|
||||
finally:command(['rm',temporary])
|
||||
for p in [nodes,*nodes.rglob('*')]:os.chown(p,65534,65534)
|
||||
work=deck/'video/comfy-work/models'
|
||||
for folder in ('diffusion_models','text_encoders','vae','latent_upscale_models','loras','Stable-Diffusion','Lora','VAE','Embeddings','controlnet','model_patches','clip','clip_vision','upscale_models','tensorrt','unet'):
|
||||
(work/folder).mkdir(parents=True,exist_ok=True);os.chown(work/folder,65534,65534)
|
||||
for meta in (deck/'models').glob('*/entry.json'):
|
||||
item=json.loads(meta.read_text());f=PurePosixPath(item['file']);source=meta.parent/('model'+f.suffix)
|
||||
if item.get('repo')=='Lightricks/LTX-2.5' and f.parts[0] in ('diffusion_models','text_encoders','vae','latent_upscale_models') and source.is_file():
|
||||
link=work/f.parts[0]/f.name
|
||||
if not link.exists() and not link.is_symlink():link.symlink_to('/var/lib/deck/models/'+meta.parent.name+'/model'+f.suffix)
|
||||
base=manager.state/'services'/name
|
||||
if base.is_symlink():raise ValueError('Unsicheres Dienstverzeichnis.')
|
||||
base.mkdir(parents=True,exist_ok=True,mode=0o700)
|
||||
args=['create','--name',name,'--network',c['network'],'--restart',c['restart'],'--cap-drop','ALL','--security-opt','no-new-privileges:true','--pids-limit','256']
|
||||
if c['memory']:args+=['--memory',str(c['memory'])]
|
||||
if c['nanocpus']:args+=['--cpus',str(c['nanocpus']/1e9)]
|
||||
for k,v in c['labels'].items():
|
||||
if not isinstance(k,str) or not k.startswith('io.athena-deck.') or not isinstance(v,str) or len(v)>512:raise ValueError('Ungültige Labels.')
|
||||
args+=['--label',k+'='+v]
|
||||
for p,bindings in c['ports'].items():
|
||||
for b in bindings:args+=['-p',b['HostIp']+':'+str(b['HostPort'])+':'+p]
|
||||
for value in c['env']:args+=['-e',value]
|
||||
if c['user']:args+=['--user',c['user']]
|
||||
if c['workdir']:args+=['--workdir',c['workdir']]
|
||||
for i,m in enumerate(c['mounts']):
|
||||
source=deck/m['deck_path'] if m['deck_path'] is not None else base/str(i)
|
||||
if source.is_symlink():raise ValueError('Unsicheres Volume-Verzeichnis.')
|
||||
if m['deck_path']:
|
||||
if m['deck_path']=='video/comfy-client-token' and not source.exists():
|
||||
import secrets
|
||||
source.parent.mkdir(parents=True,exist_ok=True);source.write_text(secrets.token_urlsafe(48)+'\n');source.chmod(0o600);os.chown(source,65534,65534)
|
||||
elif m['deck_path']!='video/comfy-client-token':source.mkdir(parents=True,exist_ok=True)
|
||||
elif m['was_file']:
|
||||
if 'file' not in m['files']:raise ValueError('Benötigte Volume-Datei fehlt im Backup.')
|
||||
source.write_bytes(base64.b64decode(m['files']['file']));source.chmod(0o600)
|
||||
else:
|
||||
source.mkdir(exist_ok=True)
|
||||
for filename,data in m['files'].items():
|
||||
target=source/filename;target.write_bytes(base64.b64decode(data));target.chmod(0o600)
|
||||
# Application UIDs commonly used by these rootless interfaces.
|
||||
uid=c.get('uid',int(c['user'].split(':')[0]) if c['user'].split(':')[0].isdigit() else 0)
|
||||
if m['deck_path'] is None:
|
||||
os.chown(source,uid,uid)
|
||||
if source.is_dir():
|
||||
for p in source.iterdir():os.chown(p,uid,uid)
|
||||
args+=['--mount',f'type=bind,src={source},dst={m["target"]}'+(',readonly' if m['read_only'] else '')]
|
||||
if c.get('entrypoint'):
|
||||
args+=['--entrypoint',c['entrypoint'][0]];entry_tail=c['entrypoint'][1:]
|
||||
else:entry_tail=[]
|
||||
args+=[c['image'] if c.get('build_recipe') else c['registry'],*entry_tail,*(c.get('cmd') or [])]
|
||||
command(args)
|
||||
if c['running']:
|
||||
command(['start',name]);time.sleep(1)
|
||||
if command(['inspect','--format','{{.State.Running}}',name])!='true':raise ValueError('Wiederhergestellter Container ist nicht gestartet geblieben.')
|
||||
return {'state':'complete','message':'Image bereit, Konfiguration und Container wiederhergestellt.'}
|
||||
@@ -143,6 +143,12 @@ class Manager:
|
||||
return self.video_status(config)
|
||||
def dispatch(self,data):
|
||||
if not isinstance(data,dict):raise ValueError('Ungültige Helferanfrage.')
|
||||
if data.get('action')=='backup-export' and set(data)=={'action'}:
|
||||
import docker_backup
|
||||
return docker_backup.export(self)
|
||||
if data.get('action')=='backup-restore' and set(data)=={'action','container'}:
|
||||
import docker_backup
|
||||
return docker_backup.restore(self,data['container'])
|
||||
if set(data)=={'action','service'} and data['action'] in ('video-start','video-stop'):
|
||||
return self.video_action(data['service'],data['action']=='video-start')
|
||||
if set(data)!={'action'}:raise ValueError('Ungültige Helferanfrage.')
|
||||
@@ -197,8 +203,8 @@ class Handler(socketserver.StreamRequestHandler):
|
||||
try:
|
||||
_,uid,_=struct.unpack('3i',self.connection.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
|
||||
if uid not in (0,self.server.client_uid):raise ValueError('Client nicht erlaubt.')
|
||||
raw=self.rfile.readline(4097)
|
||||
if len(raw)>4096:raise ValueError('Anfrage zu groß.')
|
||||
raw=self.rfile.readline(67108865)
|
||||
if len(raw)>67108864:raise ValueError('Anfrage zu groß.')
|
||||
data=json.loads(raw)
|
||||
if isinstance(data,dict) and data.get('action')=='video-http':
|
||||
self.connection.settimeout(3600);video_http(self.server.manager,data,self.rfile,self.wfile);return
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ import urllib.request
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
LABEL = 'de.casaderoll.athena-deck.standalone'
|
||||
FILES = ['deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock']
|
||||
FILES = ['backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock']
|
||||
|
||||
|
||||
def run(*args, check=True, interactive=False):
|
||||
@@ -175,7 +175,7 @@ def install(args):
|
||||
(base/sub).mkdir(mode=0o700)
|
||||
os.chown(base/'state',65534,65534)
|
||||
config=dict(owner=LABEL,base=str(base),name=args.name,port=args.port,api_ports=api_ports,image=image,previous_image=None,gpu_telemetry=args.gpu_telemetry,image_runtime=True,docker_helper=True)
|
||||
run(sys.executable,str(ROOT/'deploy/setup_docker_helper.py'),'--client-uid','65534','--client-gid','65534')
|
||||
run(sys.executable,str(ROOT/'deploy/setup_docker_helper.py'),'--client-uid','65534','--client-gid','65534','--deck-state',str(base/'state'))
|
||||
write_manifest(base,config)
|
||||
provision(config)
|
||||
try:
|
||||
@@ -255,7 +255,7 @@ def main():
|
||||
config=load_manifest(args.directory)
|
||||
item=owned(config['name'],args.directory)
|
||||
if args.docker_helper:
|
||||
run(sys.executable,str(ROOT/'deploy/setup_docker_helper.py'),'--client-uid','65534','--client-gid','65534')
|
||||
run(sys.executable,str(ROOT/'deploy/setup_docker_helper.py'),'--client-uid','65534','--client-gid','65534','--deck-state',str(args.directory/'state'))
|
||||
config['docker_helper']=True;write_manifest(args.directory,config);update(args.directory,force=True)
|
||||
elif args.status:
|
||||
print('Deck: '+('running' if item and item['State']['Running'] else 'stopped / absent'))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Provision Deck's fixed Unix-socket helper; does not install Docker itself."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -9,7 +10,7 @@ import subprocess
|
||||
MARKER='# Athena Deck Docker helper — managed by setup_docker_helper.py'
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument('--client-uid',type=int,required=True);p.add_argument('--client-gid',type=int,required=True);p.add_argument('--allow-install',action='store_true',help='Erlaubt Erstinstallation über die authentifizierte Deck-GUI auf einem unbenutzten Debian-Host.');args=p.parse_args()
|
||||
p=argparse.ArgumentParser();p.add_argument('--client-uid',type=int,required=True);p.add_argument('--client-gid',type=int,required=True);p.add_argument('--allow-install',action='store_true',help='Erlaubt Erstinstallation über die authentifizierte Deck-GUI auf einem unbenutzten Debian-Host.');p.add_argument('--deck-state');args=p.parse_args()
|
||||
if os.geteuid()!=0 or not Path('/run/systemd/system').is_dir():raise SystemExit('Als root auf dem Debian-Zielserver mit systemd ausführen.')
|
||||
if args.client_uid<1 or args.client_gid<1:raise SystemExit('Eigene unprivilegierte Deck-UID und GID erforderlich.')
|
||||
base=Path('/opt/athena-deck-docker-helper');unit=Path('/etc/systemd/system/athena-deck-docker-helper.service')
|
||||
@@ -20,6 +21,15 @@ def main():
|
||||
target=base/'docker_helper.py'
|
||||
if target.is_symlink():raise SystemExit('Symlink-Ziel nicht zulässig.')
|
||||
shutil.copyfile(Path(__file__).with_name('docker_helper.py'),target);os.chown(target,0,0);target.chmod(0o644);(base/'managed-by-deck').touch()
|
||||
shutil.copyfile(Path(__file__).with_name('docker_backup.py'),base/'docker_backup.py')
|
||||
(base/'swarm-ui').mkdir(exist_ok=True)
|
||||
for name in ('Dockerfile','patch-source.py','proxy.py','entrypoint.py'):
|
||||
shutil.copyfile(Path(__file__).parent/'swarm-ui'/name,base/'swarm-ui'/name)
|
||||
if args.deck_state:
|
||||
registered=Path(args.deck_state).resolve()
|
||||
if not registered.is_dir():raise SystemExit('Deck-Zustandsverzeichnis fehlt.')
|
||||
state=Path('/var/lib/athena-deck-docker');state.mkdir(exist_ok=True)
|
||||
config=state/'backup-policy.json';config.write_text(json.dumps({'deck_state':str(registered)}));config.chmod(0o600)
|
||||
command=f'/usr/bin/python3 {target} --client-uid {args.client_uid} --client-gid {args.client_gid}'+(' --allow-install' if args.allow_install else '')
|
||||
unit.write_text(MARKER+f'''
|
||||
[Unit]
|
||||
@@ -38,6 +48,7 @@ NoNewPrivileges=true
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem={'false' if args.allow_install else 'strict'}
|
||||
ReadWritePaths=-{args.deck_state or '/var/lib/athena-deck'}
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
''')
|
||||
@@ -45,5 +56,5 @@ WantedBy=multi-user.target
|
||||
subprocess.run(['/usr/bin/systemctl','daemon-reload'],check=True)
|
||||
subprocess.run(['/usr/bin/systemctl','enable','athena-deck-docker-helper.service'],check=True,capture_output=True)
|
||||
subprocess.run(['/usr/bin/systemctl','restart','athena-deck-docker-helper.service'],check=True)
|
||||
print('Deck-Docker-Helfer eingerichtet. Installationsrecht: '+('Erstinstallation erlaubt' if args.allow_install else 'nur lesender Bestand'))
|
||||
print('Deck-Docker-Helfer eingerichtet. Installationsrecht: '+('Erstinstallation erlaubt' if args.allow_install else 'Bestand und begrenzter Backup-Restore; keine Paketinstallation'))
|
||||
if __name__=='__main__':main()
|
||||
|
||||
Reference in New Issue
Block a user