Add scoped start stop and delete actions for application containers
This commit is contained in:
@@ -281,3 +281,13 @@ Technische Konfiguration und Profilfreigaben bleiben unter Einstellungen → End
|
|||||||
Für die derzeit unterstützten YuE2-Musikprofile führt die Steuerung direkt zu
|
Für die derzeit unterstützten YuE2-Musikprofile führt die Steuerung direkt zu
|
||||||
LadyPoly auf Port 8127, analog zum LTX-DeskWEB-Link bei Video. Die Oberfläche
|
LadyPoly auf Port 8127, analog zum LTX-DeskWEB-Link bei Video. Die Oberfläche
|
||||||
kann bereits vor dem Start geöffnet werden; Generierung benötigt Musikbetrieb.
|
kann bereits vor dem Start geöffnet werden; Generierung benötigt Musikbetrieb.
|
||||||
|
|
||||||
|
### Anwendungscontainer verwalten
|
||||||
|
|
||||||
|
Weitere Dienste bietet Starten, Stoppen und Löschen für Container mit beiden
|
||||||
|
Deck-Anwendungslabels. Der begrenzte Systemhelfer prüft die aktuelle Zuordnung
|
||||||
|
bei jeder Aktion anhand der vollständigen Container-ID. Registrierte Video-
|
||||||
|
Laufzeiten müssen weiter über die Steuerung verwaltet werden. Löschen benötigt
|
||||||
|
einen gestoppten Container und eine Bestätigung; weder Images noch Volumes
|
||||||
|
werden gelöscht. Daten nur in der beschreibbaren Container-Schicht gehen verloren.
|
||||||
|
Es gibt keine pauschalen Stop-, Prune- oder Löschaktionen für den Docker-Stack.
|
||||||
|
|||||||
@@ -141,8 +141,21 @@ class Manager:
|
|||||||
result=run(['/usr/bin/docker','start',config['container_id']] if start else ['/usr/bin/docker','stop','--time','30',config['container_id']],timeout=45)
|
result=run(['/usr/bin/docker','start',config['container_id']] if start else ['/usr/bin/docker','stop','--time','30',config['container_id']],timeout=45)
|
||||||
if result.returncode:raise ValueError('Videodienst konnte nicht gestartet oder beendet werden.')
|
if result.returncode:raise ValueError('Videodienst konnte nicht gestartet oder beendet werden.')
|
||||||
return self.video_status(config)
|
return self.video_status(config)
|
||||||
|
def container_action(self,ident,action):
|
||||||
|
if action not in ('start','stop','delete') or not isinstance(ident,str) or not re.fullmatch('[a-f0-9]{64}',ident):raise ValueError('Ungültige Containeraktion.')
|
||||||
|
with self.lock:
|
||||||
|
row=next((c for c in self.inventory() if c['id']==ident),None)
|
||||||
|
if row is None:raise ValueError('Container nicht für Deck freigegeben oder nicht mehr vorhanden.')
|
||||||
|
if any(c['container_id']==ident for c in self.video_configs()):raise ValueError('Registrierte Video-Laufzeiten über die Steuerung verwalten.')
|
||||||
|
if action=='delete' and row['state'] not in ('exited','created','dead'):raise ValueError('Container vor dem Löschen stoppen.')
|
||||||
|
args=['/usr/bin/docker']+(['stop','--time','30',ident] if action=='stop' else ['rm',ident] if action=='delete' else ['start',ident])
|
||||||
|
result=run(args,timeout=45)
|
||||||
|
if result.returncode:raise ValueError('Containeraktion fehlgeschlagen. Status erneut prüfen.')
|
||||||
|
return dict(changed=True,action=action,id=ident)
|
||||||
def dispatch(self,data):
|
def dispatch(self,data):
|
||||||
if not isinstance(data,dict):raise ValueError('Ungültige Helferanfrage.')
|
if not isinstance(data,dict):raise ValueError('Ungültige Helferanfrage.')
|
||||||
|
if set(data)=={'action','service'} and data['action'] in ('container-start','container-stop','container-delete'):
|
||||||
|
return self.container_action(data['service'],data['action'].removeprefix('container-'))
|
||||||
if data.get('action')=='backup-export' and set(data)=={'action'}:
|
if data.get('action')=='backup-export' and set(data)=={'action'}:
|
||||||
import docker_backup
|
import docker_backup
|
||||||
return docker_backup.export(self)
|
return docker_backup.export(self)
|
||||||
|
|||||||
+14
-4
@@ -2,13 +2,23 @@ window.DockerUI=(()=>{
|
|||||||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
async function api(action='',data){const r=await fetch('/api/v1/docker'+action,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});if(r.status===401){location.reload();throw Error('Bitte erneut anmelden.');}const value=await r.json();if(!r.ok)throw Error(value.error||'Docker-Anfrage fehlgeschlagen');return value;}
|
async function api(action='',data){const r=await fetch('/api/v1/docker'+action,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});if(r.status===401){location.reload();throw Error('Bitte erneut anmelden.');}const value=await r.json();if(!r.ok)throw Error(value.error||'Docker-Anfrage fehlgeschlagen');return value;}
|
||||||
function html(services=false){return `<div class="kicker">${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${services?'Weitere Dienste':'Docker'}</h1><p>${services?'Zusätzliche Oberflächen werden getrennt von Modellen und Laufzeiten verwaltet. Hier erscheinen ausschließlich ausdrücklich für Deck markierte Anwendungscontainer.':'Optionale Umgebung für zusätzliche Studios. Native Modelllaufzeiten bleiben unabhängig davon.'}</p><section id="docker-panel"></section><p id="docker-message" role="status"></p>`;}
|
function html(services=false){return `<div class="kicker">${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${services?'Weitere Dienste':'Docker'}</h1><p>${services?'Zusätzliche Oberflächen werden getrennt von Modellen und Laufzeiten verwaltet. Hier erscheinen ausschließlich ausdrücklich für Deck markierte Anwendungscontainer.':'Optionale Umgebung für zusätzliche Studios. Native Modelllaufzeiten bleiben unabhängig davon.'}</p><section id="docker-panel"></section><p id="docker-message" role="status"></p>`;}
|
||||||
function bind(services=false){const panel=document.querySelector('#docker-panel'),message=document.querySelector('#docker-message');let signature='',pending=false;
|
function bind(services=false){const panel=document.querySelector('#docker-panel'),message=document.querySelector('#docker-message');let signature='',pending=false,timer;
|
||||||
async function refresh(){try{const s=await api();if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running';
|
async function refresh(){clearTimeout(timer);try{if(pending)return;const s=await api();if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running';
|
||||||
if(services){panel.innerHTML=`<section class="card"><h2>Explizite Zuordnung</h2><p>Beide Container-Labels müssen gesetzt sein:</p><pre><code>io.athena-deck.managed=true\nio.athena-deck.role=application</code></pre><p class="small">Keine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Diese Ansicht verändert keine Container oder Modelldateien.</p><a class="link" href="#docker-runtime">Docker-Verbindung prüfen →</a></section>${!s.helper_available||!s.reachable?'<section class="empty"><h2>Docker nicht verbunden</h2><p>'+esc(s.message)+'</p></section>':s.services.length?'<div class="grid">'+s.services.map(c=>`<section class="card"><span class="pill">DOCKER · DECK-ANWENDUNG</span><h2>${esc(c.name)}</h2><p>${esc(c.image)}</p><p><strong>${esc(c.state)}</strong> · ${esc(c.status)}</p>${c.name==='athena-ladypoly'?`<p>LadyPoly YuE2 · Oberfläche für Decks Musiklaufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly öffnen →</a><p class="small">Zuerst Deck-Verbindung einrichten und in der Übersicht Musik aktivieren. SSH-Tunnel: Port 8127.</p>`:''}${c.name==='athena-swarm-ui'?`<p>API By URL · Deck steuert die Video-Laufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8125/')}" target="_blank" rel="noopener">SwarmUI öffnen →</a><p class="small">In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.</p>`:''}</section>`).join('')+'</div>':'<section class="empty"><h2>Noch keine Deck-Anwendungen</h2><p>Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.</p></section>'}`;}
|
if(services){panel.innerHTML=`<section class="card"><h2>Explizite Zuordnung</h2><p>Beide Container-Labels müssen gesetzt sein:</p><pre><code>io.athena-deck.managed=true\nio.athena-deck.role=application</code></pre><p class="small">Keine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Starten und Stoppen betrifft nur markierte Container. Löschen entfernt den Container einschließlich seiner beschreibbaren Schicht; Images und Volumes bleiben erhalten.</p><a class="link" href="#docker-runtime">Docker-Verbindung prüfen →</a></section>${!s.helper_available||!s.reachable?'<section class="empty"><h2>Docker nicht verbunden</h2><p>'+esc(s.message)+'</p></section>':s.services.length?'<div class="grid">'+s.services.map(c=>`<section class="card"><span class="pill">DOCKER · DECK-ANWENDUNG</span><h2>${esc(c.name)}</h2><p>${esc(c.image)}</p><p><strong>${esc(c.state)}</strong> · ${esc(c.status)}</p>${c.name==='athena-ladypoly'?`<p>LadyPoly YuE2 · Oberfläche für Decks Musiklaufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly öffnen →</a><p class="small">Zuerst Deck-Verbindung einrichten und in der Übersicht Musik aktivieren. SSH-Tunnel: Port 8127.</p>`:''}${c.name==='athena-swarm-ui'?`<p>API By URL · Deck steuert die Video-Laufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8125/')}" target="_blank" rel="noopener">SwarmUI öffnen →</a><p class="small">In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.</p>`:''}<div class="card-actions"><button data-container-action="start" data-container-id="${esc(c.id)}" ${c.state==='running'?'disabled':''}>Starten</button><button class="secondary" data-container-action="stop" data-container-id="${esc(c.id)}" ${c.state!=='running'?'disabled':''}>Stoppen</button><button class="secondary danger" data-container-action="delete" data-container-id="${esc(c.id)}" ${!['exited','created','dead'].includes(c.state)?'disabled':''}>Löschen</button></div></section>`).join('')+'</div>' :'<section class="empty"><h2>Noch keine Deck-Anwendungen</h2><p>Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.</p></section>'}`;}
|
||||||
else {panel.innerHTML=`<section class="card"><span class="pill">${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}</span><h2>Docker Engine ${esc(s.version||'')}</h2><p>${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}</p><p>${esc(s.message)}</p>${s.job?`<p role="status">${esc(s.job.state)} · ${esc(s.job.phase)}</p>`:''}${!s.installed&&s.install_supported&&!running?'<label><input type="checkbox" id="docker-install-ack"> Docker Engine, Compose und Buildx auf diesem Server installieren. Docker richtet einen Dienst und Netzwerk-/Firewallregeln ein.</label><p><button id="docker-install" disabled>Docker installieren</button></p>':`<button disabled>${running?'Installation läuft …':s.installed?'Docker bereits vorhanden':'Installation hier nicht verfügbar'}</button>`}<p class="small">Keine automatische Aktualisierung vorhandener Docker-Installationen. NVIDIA Container Toolkit und Treiber sind separate Voraussetzungen für GPU-Container. Die Paketinstallation wird nicht mitten im Vorgang abgebrochen.</p><a class="link" href="#services">Weitere Dienste öffnen →</a></section>`;
|
else {panel.innerHTML=`<section class="card"><span class="pill">${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}</span><h2>Docker Engine ${esc(s.version||'')}</h2><p>${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}</p><p>${esc(s.message)}</p>${s.job?`<p role="status">${esc(s.job.state)} · ${esc(s.job.phase)}</p>`:''}${!s.installed&&s.install_supported&&!running?'<label><input type="checkbox" id="docker-install-ack"> Docker Engine, Compose und Buildx auf diesem Server installieren. Docker richtet einen Dienst und Netzwerk-/Firewallregeln ein.</label><p><button id="docker-install" disabled>Docker installieren</button></p>':`<button disabled>${running?'Installation läuft …':s.installed?'Docker bereits vorhanden':'Installation hier nicht verfügbar'}</button>`}<p class="small">Keine automatische Aktualisierung vorhandener Docker-Installationen. NVIDIA Container Toolkit und Treiber sind separate Voraussetzungen für GPU-Container. Die Paketinstallation wird nicht mitten im Vorgang abgebrochen.</p><a class="link" href="#services">Weitere Dienste öffnen →</a></section>`;
|
||||||
const button=panel.querySelector('#docker-install'),ack=panel.querySelector('#docker-install-ack');if(button){ack.onchange=()=>button.disabled=pending||!ack.checked;button.onclick=async()=>{pending=true;button.disabled=true;try{await api('/install',{confirm:true});message.textContent='Docker-Erstinstallation gestartet.';}catch(error){message.textContent=error.message;}finally{pending=false;signature='';}};}
|
const button=panel.querySelector('#docker-install'),ack=panel.querySelector('#docker-install-ack');if(button){ack.onchange=()=>button.disabled=pending||!ack.checked;button.onclick=async()=>{pending=true;button.disabled=true;try{await api('/install',{confirm:true});message.textContent='Docker-Erstinstallation gestartet.';}catch(error){message.textContent=error.message;}finally{pending=false;signature='';}};}
|
||||||
}
|
}
|
||||||
}}catch(error){if(panel.isConnected){message.textContent=error.message;signature='';}}finally{if(panel.isConnected)setTimeout(refresh,5000);}}
|
}
|
||||||
|
if(services)panel.querySelectorAll('[data-container-action]').forEach(b=>b.onclick=async()=>{
|
||||||
|
const action=b.dataset.containerAction,id=b.dataset.containerId,c=s.services.find(c=>c.id===id);
|
||||||
|
if(pending||!c)return;
|
||||||
|
if(action==='delete'&&!confirm(`Container „${c.name}“ endgültig löschen? Die beschreibbare Container-Schicht geht verloren. Images und Volumes bleiben erhalten.`))return;
|
||||||
|
pending=true;panel.querySelectorAll('button').forEach(x=>x.disabled=true);message.textContent=action==='stop'?'Container wird gestoppt (bis zu 30 Sekunden) …':action==='start'?'Container wird gestartet …':'Container wird gelöscht …';
|
||||||
|
try{await api('/'+action,{id,...(action==='delete'?{confirm:true}:{})});message.textContent=action==='delete'?'Container gelöscht. Images und Volumes bleiben erhalten.':action==='start'?'Container gestartet.':'Container gestoppt.';}
|
||||||
|
catch(error){message.textContent=error.message;}
|
||||||
|
finally{pending=false;signature='';if(panel.isConnected)refresh();}
|
||||||
|
});
|
||||||
|
}catch(error){if(panel.isConnected){message.textContent=error.message;signature='';}}finally{if(panel.isConnected)timer=setTimeout(refresh,5000);}}
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
return {html,bind};
|
return {html,bind};
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ class DockerSupport:
|
|||||||
try:return dict(self.call('status'),helper_available=True)
|
try:return dict(self.call('status'),helper_available=True)
|
||||||
except (OSError,ValueError):return dict(helper_available=False,installed=None,reachable=False,services=[],install_supported=False,job=None,message='Docker-Systemhelfer nicht verbunden. Der Deck-Installer muss ihn auf dem Debian-Host einrichten.',labels=[MANAGED_LABEL,ROLE_LABEL])
|
except (OSError,ValueError):return dict(helper_available=False,installed=None,reachable=False,services=[],install_supported=False,job=None,message='Docker-Systemhelfer nicht verbunden. Der Deck-Installer muss ihn auf dem Debian-Host einrichten.',labels=[MANAGED_LABEL,ROLE_LABEL])
|
||||||
def install(self):return self.call('install')
|
def install(self):return self.call('install')
|
||||||
|
|
||||||
|
def container_action(self,action,ident):
|
||||||
|
if action not in ('start','stop','delete'):raise ValueError('Ungültige Containeraktion.')
|
||||||
|
return self.call('container-'+action,ident)
|
||||||
|
|||||||
@@ -488,6 +488,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
with self.server.auth_lock:
|
with self.server.auth_lock:
|
||||||
self.server.sessions.pop(self.session_token(),None)
|
self.server.sessions.pop(self.session_token(),None)
|
||||||
return self.respond({'logged_out':True})
|
return self.respond({'logged_out':True})
|
||||||
|
if self.path in ('/api/v1/docker/start','/api/v1/docker/stop','/api/v1/docker/delete'):
|
||||||
|
try:
|
||||||
|
data=self.read_json();action=self.path.rsplit('/',1)[1]
|
||||||
|
expected={'id','confirm'} if action=='delete' else {'id'}
|
||||||
|
if set(data)!=expected or (action=='delete' and data['confirm'] is not True):raise ValueError('Ungültige oder unbestätigte Containeraktion.')
|
||||||
|
return self.respond(self.server.docker.container_action(action,data['id']))
|
||||||
|
except (OSError,ValueError) as exc:return self.respond({'error':str(exc) if isinstance(exc,ValueError) else 'Docker-Systemhelfer nicht erreichbar.'},400)
|
||||||
if self.path == '/api/v1/docker/install':
|
if self.path == '/api/v1/docker/install':
|
||||||
try:
|
try:
|
||||||
if self.read_json()!={'confirm':True}:raise ValueError('Die Docker-Erstinstallation muss ausdrücklich bestätigt werden.')
|
if self.read_json()!={'confirm':True}:raise ValueError('Die Docker-Erstinstallation muss ausdrücklich bestätigt werden.')
|
||||||
|
|||||||
@@ -21,6 +21,23 @@ class DockerTests(unittest.TestCase):
|
|||||||
manager=Manager(d)
|
manager=Manager(d)
|
||||||
for data in ({'action':'exec','command':'id'},{'action':'stop'},[],{'action':'install','args':['--privileged']}):
|
for data in ({'action':'exec','command':'id'},{'action':'stop'},[],{'action':'install','args':['--privileged']}):
|
||||||
with self.assertRaises(ValueError):manager.dispatch(data)
|
with self.assertRaises(ValueError):manager.dispatch(data)
|
||||||
|
def test_container_actions_require_current_labelled_full_id(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
manager=Manager(d);ident='a'*64
|
||||||
|
with patch.object(manager,'inventory',return_value=[]),patch('deploy.docker_helper.run') as run:
|
||||||
|
with self.assertRaises(ValueError):manager.container_action(ident,'stop')
|
||||||
|
run.assert_not_called()
|
||||||
|
with patch.object(manager,'inventory',return_value=[dict(id=ident,state='running')]),patch.object(manager,'video_configs',return_value=[]),patch('deploy.docker_helper.run') as run:
|
||||||
|
with self.assertRaises(ValueError):manager.container_action(ident,'delete')
|
||||||
|
run.assert_not_called()
|
||||||
|
with patch.object(manager,'inventory',return_value=[dict(id=ident,state='exited')]),patch.object(manager,'video_configs',return_value=[]),patch('deploy.docker_helper.run',return_value=Mock(returncode=0)) as run:
|
||||||
|
for action,command in [('start',['start',ident]),('stop',['stop','--time','30',ident]),('delete',['rm',ident])]:
|
||||||
|
self.assertTrue(manager.dispatch({'action':'container-'+action,'service':ident})['changed'])
|
||||||
|
self.assertEqual(run.call_args.args[0],['/usr/bin/docker']+command)
|
||||||
|
with patch.object(manager,'inventory',return_value=[dict(id=ident,state='exited')]),patch.object(manager,'video_configs',return_value=[dict(container_id=ident)]),patch('deploy.docker_helper.run') as run:
|
||||||
|
with self.assertRaises(ValueError):manager.container_action(ident,'start')
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
def test_existing_docker_is_never_reinstalled(self):
|
def test_existing_docker_is_never_reinstalled(self):
|
||||||
with tempfile.TemporaryDirectory() as d,patch('deploy.docker_helper.release',return_value={'ID':'debian','VERSION_ID':'13'}),patch('deploy.docker_helper.Path.exists',return_value=True),patch('deploy.docker_helper.threading.Thread') as thread:
|
with tempfile.TemporaryDirectory() as d,patch('deploy.docker_helper.release',return_value={'ID':'debian','VERSION_ID':'13'}),patch('deploy.docker_helper.Path.exists',return_value=True),patch('deploy.docker_helper.threading.Thread') as thread:
|
||||||
manager=Manager.__new__(Manager);manager.allow_install=True;manager.lock=__import__('threading').RLock();manager.job=None
|
manager=Manager.__new__(Manager);manager.allow_install=True;manager.lock=__import__('threading').RLock();manager.job=None
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ class Tests(unittest.TestCase):
|
|||||||
self.assertEqual(runtime.call_count,1)
|
self.assertEqual(runtime.call_count,1)
|
||||||
self.assertTrue(all('execution' in row and 'used_by_profiles' in row for row in result['entries']))
|
self.assertTrue(all('execution' in row and 'used_by_profiles' in row for row in result['entries']))
|
||||||
|
|
||||||
|
def test_container_actions_require_admin_and_delete_confirmation(self):
|
||||||
|
ident='a'*64
|
||||||
|
with patch.object(self.server.docker,'container_action',return_value={'changed':True}) as action:
|
||||||
|
for operation in ('start','stop','delete'):
|
||||||
|
data={'id':ident,**({'confirm':True} if operation=='delete' else {})}
|
||||||
|
req=urllib.request.Request(self.url+'/api/v1/docker/'+operation,data=json.dumps(data).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
|
||||||
|
with urllib.request.urlopen(req) as response:self.assertTrue(json.load(response)['changed'])
|
||||||
|
action.assert_called_with(operation,ident)
|
||||||
|
count=action.call_count
|
||||||
|
req=urllib.request.Request(self.url+'/api/v1/docker/delete',data=json.dumps({'id':ident}).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
|
||||||
|
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
|
||||||
|
self.assertEqual(error.exception.code,400);error.exception.close()
|
||||||
|
req=urllib.request.Request(self.url+'/api/v1/docker/start',data=json.dumps({'id':ident}).encode(),headers={'X-Athena-Deck':'1','Content-Type':'application/json'})
|
||||||
|
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
|
||||||
|
self.assertEqual(error.exception.code,401);error.exception.close()
|
||||||
|
self.assertEqual(action.call_count,count)
|
||||||
|
|
||||||
def test_demo_removed_from_status(self):
|
def test_demo_removed_from_status(self):
|
||||||
state=self.request('status')
|
state=self.request('status')
|
||||||
self.assertNotIn('demo',state)
|
self.assertNotIn('demo',state)
|
||||||
|
|||||||
Reference in New Issue
Block a user