Compare commits
No commits in common. "7b2c93bcefd96f5d72a099d43b96c8c82e4fefd5" and "7b08e6a45ef1f2347294c926eb49dbec70024394" have entirely different histories.
7b2c93bcef
...
7b08e6a45e
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
snek-container-compose.yml
|
|
||||||
.r_history
|
.r_history
|
||||||
.vscode
|
.vscode
|
||||||
.history
|
.history
|
||||||
|
|||||||
@ -51,7 +51,6 @@ from snek.view.register import RegisterView
|
|||||||
from snek.view.repository import RepositoryView
|
from snek.view.repository import RepositoryView
|
||||||
from snek.view.rpc import RPCView
|
from snek.view.rpc import RPCView
|
||||||
from snek.view.search_user import SearchUserView
|
from snek.view.search_user import SearchUserView
|
||||||
from snek.view.container import ContainerView
|
|
||||||
from snek.view.settings.containers import (
|
from snek.view.settings.containers import (
|
||||||
ContainersCreateView,
|
ContainersCreateView,
|
||||||
ContainersDeleteView,
|
ContainersDeleteView,
|
||||||
@ -251,7 +250,6 @@ class Application(BaseApplication):
|
|||||||
show_index=True,
|
show_index=True,
|
||||||
)
|
)
|
||||||
self.router.add_view("/profiler.html", profiler_handler)
|
self.router.add_view("/profiler.html", profiler_handler)
|
||||||
self.router.add_view("/container/sock/{channel_uid}.json", ContainerView)
|
|
||||||
self.router.add_view("/about.html", AboutHTMLView)
|
self.router.add_view("/about.html", AboutHTMLView)
|
||||||
self.router.add_view("/about.md", AboutMDView)
|
self.router.add_view("/about.md", AboutMDView)
|
||||||
self.router.add_view("/logout.json", LogoutView)
|
self.router.add_view("/logout.json", LogoutView)
|
||||||
|
|||||||
@ -9,46 +9,20 @@ class ContainerService(BaseService):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.compose_path = "snek-container-compose.yml"
|
self.compose_path = "snek-container-compose.yml"
|
||||||
self.compose = ComposeFileManager(self.compose_path,self.container_event_handler)
|
self.compose = ComposeFileManager(self.compose_path)
|
||||||
self.event_listeners = {}
|
|
||||||
|
|
||||||
async def add_event_listener(self, name, event,event_handler):
|
|
||||||
if not name in self.event_listeners:
|
|
||||||
self.event_listeners[name] = {}
|
|
||||||
if not event in self.event_listeners[name]:
|
|
||||||
self.event_listeners[name][event] = []
|
|
||||||
self.event_listeners[name][event].append(event_handler)
|
|
||||||
|
|
||||||
async def container_event_handler(self, name, event, data):
|
|
||||||
event_listeners = self.event_listeners.get(name, {})
|
|
||||||
handlers = event_listeners.get(event, [])
|
|
||||||
for handler in handlers:
|
|
||||||
if not await handler(data):
|
|
||||||
handlers.remove(handler)
|
|
||||||
|
|
||||||
async def get_instances(self):
|
async def get_instances(self):
|
||||||
return list(self.compose.list_instances())
|
return list(self.compose.list_instances())
|
||||||
|
|
||||||
async def get_container_name(self, channel_uid):
|
async def get_container_name(self, channel_uid):
|
||||||
if channel_uid.startswith("channel-"):
|
|
||||||
return channel_uid
|
|
||||||
return f"channel-{channel_uid}"
|
return f"channel-{channel_uid}"
|
||||||
|
|
||||||
async def get(self,channel_uid):
|
async def get(self,channel_uid):
|
||||||
return await self.compose.get_instance(await self.get_container_name(channel_uid))
|
return await self.compose.get_instance(await self.get_container_name(channel_uid))
|
||||||
|
|
||||||
async def stop(self, channel_uid):
|
|
||||||
return await self.compose.stop(await self.get_container_name(channel_uid))
|
|
||||||
|
|
||||||
async def start(self, channel_uid):
|
|
||||||
return await self.compose.start(await self.get_container_name(channel_uid))
|
|
||||||
|
|
||||||
async def get_status(self, channel_uid):
|
async def get_status(self, channel_uid):
|
||||||
return await self.compose.get_instance_status(await self.get_container_name(channel_uid))
|
return await self.compose.get_instance_status(await self.get_container_name(channel_uid))
|
||||||
|
|
||||||
async def write_stdin(self, channel_uid, data):
|
|
||||||
return await self.compose.write_stdin(await self.get_container_name(channel_uid), data)
|
|
||||||
|
|
||||||
async def create(
|
async def create(
|
||||||
self,
|
self,
|
||||||
channel_uid,
|
channel_uid,
|
||||||
|
|||||||
@ -9,10 +9,6 @@ html {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
import { app } from "./app.js";
|
|
||||||
import { EventHandler } from "./event-handler.js";
|
|
||||||
|
|
||||||
export class Container extends EventHandler{
|
|
||||||
status = "unknown"
|
|
||||||
cpus = 0
|
|
||||||
memory = "0m"
|
|
||||||
image = "unknown:unknown"
|
|
||||||
name = null
|
|
||||||
channelUid = null
|
|
||||||
log = false
|
|
||||||
bytesSent = 0
|
|
||||||
bytesReceived = 0
|
|
||||||
_container = null
|
|
||||||
render(el){
|
|
||||||
if(this._container == null){
|
|
||||||
this._container = el
|
|
||||||
this.terminal.open(this._container)
|
|
||||||
|
|
||||||
this.terminal.onData(data => this.ws.send(new TextEncoder().encode(data)));
|
|
||||||
}
|
|
||||||
this._fitAddon.fit();
|
|
||||||
this.refresh()
|
|
||||||
|
|
||||||
}
|
|
||||||
refresh(){
|
|
||||||
this._fitAddon.fit();
|
|
||||||
this.terminal.write("\x0C");
|
|
||||||
}
|
|
||||||
toggle(){
|
|
||||||
this._container.classList.toggle("hidden")
|
|
||||||
this.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(channelUid,log){
|
|
||||||
super()
|
|
||||||
this.terminal = new Terminal({ cursorBlink: true });
|
|
||||||
this._fitAddon = new FitAddon.FitAddon();
|
|
||||||
this.terminal.loadAddon(this._fitAddon);
|
|
||||||
window.addEventListener("resize", () => this._fitAddon.fit());
|
|
||||||
this.log = log ? true : false
|
|
||||||
this.channelUid = channelUid
|
|
||||||
this.update()
|
|
||||||
this.addEventListener("stdout", (data) => {
|
|
||||||
this.bytesReceived += data.length
|
|
||||||
if(this.log){
|
|
||||||
console.log(`Container ${this.name}: ${data}`)
|
|
||||||
}
|
|
||||||
const fixedData = new Uint8Array(data);
|
|
||||||
this.terminal.write(new TextDecoder().decode(fixedData));
|
|
||||||
|
|
||||||
})
|
|
||||||
this.ws = new WebSocket(`/container/sock/${channelUid}.json`)
|
|
||||||
this.ws.binaryType = "arraybuffer"; // Support binary data
|
|
||||||
this.ws.onmessage = (event) => {
|
|
||||||
this.emit("stdout", event.data)
|
|
||||||
}
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
this.refresh()
|
|
||||||
}
|
|
||||||
window.container = this
|
|
||||||
|
|
||||||
}
|
|
||||||
async start(){
|
|
||||||
const result = await app.rpc.startContainer(this.channelUid)
|
|
||||||
await this.refresh()
|
|
||||||
return result && this.status == 'running'
|
|
||||||
}
|
|
||||||
async stop(){
|
|
||||||
const result = await app.rpc.stopContainer(this.channelUid)
|
|
||||||
await this.refresh()
|
|
||||||
return result && this.status == 'stopped'
|
|
||||||
}
|
|
||||||
async write(data){
|
|
||||||
await this.ws.send(data)
|
|
||||||
this.bytesSent += data.length
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
async update(){
|
|
||||||
|
|
||||||
const container = await app.rpc.getContainer(this.channelUid)
|
|
||||||
this.status = container["status"]
|
|
||||||
this.cpus = container["cpus"]
|
|
||||||
this.memory = container["memory"]
|
|
||||||
this.image = container["image"]
|
|
||||||
this.name = container["name"]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
window.getContainer = function(){
|
|
||||||
return new Container(app.channelUid)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@ -1,15 +1,16 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import copy
|
||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
class ComposeFileManager:
|
class ComposeFileManager:
|
||||||
def __init__(self, compose_path="docker-compose.yml",event_handler=None):
|
def __init__(self, compose_path="docker-compose.yml"):
|
||||||
self.compose_path = compose_path
|
self.compose_path = compose_path
|
||||||
self._load()
|
self._load()
|
||||||
self.running_instances = {}
|
|
||||||
self.event_handler = event_handler
|
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
try:
|
try:
|
||||||
@ -25,25 +26,6 @@ class ComposeFileManager:
|
|||||||
def list_instances(self):
|
def list_instances(self):
|
||||||
return list(self.compose.get("services", {}).keys())
|
return list(self.compose.get("services", {}).keys())
|
||||||
|
|
||||||
async def _create_readers(self, container_name):
|
|
||||||
instance = await self.get_instance(container_name)
|
|
||||||
if not instance:
|
|
||||||
return False
|
|
||||||
proc = self.running_instances.get(container_name)
|
|
||||||
if not proc:
|
|
||||||
return False
|
|
||||||
async def reader(event_handler,stream):
|
|
||||||
while True:
|
|
||||||
line = await stream.readline()
|
|
||||||
print("XXX",line)
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
await event_handler(container_name,"stdout",line)
|
|
||||||
await self.stop(container_name)
|
|
||||||
asyncio.create_task(reader(self.event_handler,proc.stdout))
|
|
||||||
asyncio.create_task(reader(self.event_handler,proc.stderr))
|
|
||||||
|
|
||||||
|
|
||||||
def create_instance(
|
def create_instance(
|
||||||
self,
|
self,
|
||||||
name,
|
name,
|
||||||
@ -57,7 +39,8 @@ class ComposeFileManager:
|
|||||||
service = {
|
service = {
|
||||||
"image": image,
|
"image": image,
|
||||||
}
|
}
|
||||||
service["command"] = command or "tail -f /dev/null"
|
if command:
|
||||||
|
service["command"] = command
|
||||||
if cpus or memory:
|
if cpus or memory:
|
||||||
service["deploy"] = {"resources": {"limits": {}}}
|
service["deploy"] = {"resources": {"limits": {}}}
|
||||||
if cpus:
|
if cpus:
|
||||||
@ -86,7 +69,7 @@ class ComposeFileManager:
|
|||||||
instance['status'] = await self.get_instance_status(name)
|
instance['status'] = await self.get_instance_status(name)
|
||||||
print("INSTANCE",instance)
|
print("INSTANCE",instance)
|
||||||
|
|
||||||
return json.loads(json.dumps(instance,default=str))
|
return instance
|
||||||
|
|
||||||
def duplicate_instance(self, name, new_name):
|
def duplicate_instance(self, name, new_name):
|
||||||
orig = self.get_instance(name)
|
orig = self.get_instance(name)
|
||||||
@ -117,88 +100,8 @@ class ComposeFileManager:
|
|||||||
stdout, _ = await proc.communicate()
|
stdout, _ = await proc.communicate()
|
||||||
running_services = stdout.decode().split()
|
running_services = stdout.decode().split()
|
||||||
return "running" if name in running_services else "stopped"
|
return "running" if name in running_services else "stopped"
|
||||||
|
# Storage size is not tracked in compose files; would need Docker API for that.
|
||||||
|
|
||||||
async def write_stdin(self, name, data):
|
|
||||||
await self.event_handler(name, "stdin", data)
|
|
||||||
proc = self.running_instances.get(name)
|
|
||||||
print("Found proc:",proc)
|
|
||||||
print(name,data)
|
|
||||||
if not proc:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
proc.stdin.write(data.encode())
|
|
||||||
return True
|
|
||||||
except Exception as ex:
|
|
||||||
print(ex)
|
|
||||||
await self.stop(name)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def stop(self, name):
|
|
||||||
"""Asynchronously stop a container by doing 'docker compose stop [name]'."""
|
|
||||||
if name not in self.list_instances():
|
|
||||||
return False
|
|
||||||
status = await self.get_instance_status(name)
|
|
||||||
if status != "running":
|
|
||||||
return True
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
"docker", "compose", "-f", self.compose_path, "stop", name,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
if name in self.running_instances:
|
|
||||||
del self.running_instances[name]
|
|
||||||
stdout, stderr = await proc.communicate()
|
|
||||||
if proc.returncode != 0:
|
|
||||||
raise RuntimeError(f"Failed to stop {name}: {stderr.decode()}")
|
|
||||||
if stdout:
|
|
||||||
await self.event_handler(name,"stdout",stdout)
|
|
||||||
return stdout.decode(errors="ignore")
|
|
||||||
|
|
||||||
await self.event_handler(name,"stdout",stderr)
|
|
||||||
return stderr.decode(errors="ignore")
|
|
||||||
|
|
||||||
async def start(self, name):
|
|
||||||
"""Asynchronously start a container by doing 'docker compose up -d [name]'."""
|
|
||||||
if name not in self.list_instances():
|
|
||||||
return False
|
|
||||||
|
|
||||||
status = await self.get_instance_status(name)
|
|
||||||
if name in self.running_instances and status == "running" and self.running_instances.get(name):
|
|
||||||
await self.stop(name)
|
|
||||||
return True
|
|
||||||
elif name in self.running_instances:
|
|
||||||
del self.running_instances[name]
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
"docker", "compose", "-f", self.compose_path, "up", name, "-d",
|
|
||||||
stdin=asyncio.subprocess.PIPE,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
stdout,stderr = await proc.communicate()
|
|
||||||
print(stdout, stderr)
|
|
||||||
if proc.returncode != 0:
|
|
||||||
print(f"Failed to start {name}: {stderr.decode(errors='ignore')}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
"docker", "compose", "-f", self.compose_path, "exec", name, "/bin/bash",
|
|
||||||
stdin=asyncio.subprocess.PIPE,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
# stdin,stderr = await proc.communicate()
|
|
||||||
self.running_instances[name] = proc
|
|
||||||
#if stdout:
|
|
||||||
# await self.event_handler(name, "stdout", stdout)
|
|
||||||
#if stderr:
|
|
||||||
# await self.event_handler(name,"stdout",stderr)
|
|
||||||
|
|
||||||
await self._create_readers(name)
|
|
||||||
|
|
||||||
return True
|
|
||||||
#return stdout and stdout.decode(errors="ignore") or stderr.decode(errors="ignore")
|
|
||||||
|
|
||||||
# Example usage:
|
# Example usage:
|
||||||
# mgr = ComposeFileManager()
|
# mgr = ComposeFileManager()
|
||||||
@ -206,5 +109,3 @@ class ComposeFileManager:
|
|||||||
# print(mgr.list_instances())
|
# print(mgr.list_instances())
|
||||||
# mgr.duplicate_instance('web', 'web_copy')
|
# mgr.duplicate_instance('web', 'web_copy')
|
||||||
# mgr.remove_instance('web_copy')
|
# mgr.remove_instance('web_copy')
|
||||||
# await mgr.start('web')
|
|
||||||
|
|
||||||
|
|||||||
@ -17,11 +17,6 @@
|
|||||||
<script src="/user-list.js"></script>
|
<script src="/user-list.js"></script>
|
||||||
<script src="/message-list.js" type="module"></script>
|
<script src="/message-list.js" type="module"></script>
|
||||||
<script src="/chat-input.js" type="module"></script>
|
<script src="/chat-input.js" type="module"></script>
|
||||||
<script src="/container.js" type="module"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/sandbox.css">
|
<link rel="stylesheet" href="/sandbox.css">
|
||||||
<link rel="stylesheet" href="/user-list.css">
|
<link rel="stylesheet" href="/user-list.css">
|
||||||
<link rel="stylesheet" href="/fa640.all.min.css">
|
<link rel="stylesheet" href="/fa640.all.min.css">
|
||||||
@ -55,13 +50,7 @@
|
|||||||
<chat-window class="chat-area"></chat-window>
|
<chat-window class="chat-area"></chat-window>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
<script type="module">
|
<script>
|
||||||
import { app } from "/app.js";
|
|
||||||
import { Container } from "/container.js";
|
|
||||||
app.channelUid = '{{ channel.uid.value }}'
|
|
||||||
window.getContainer = function(){
|
|
||||||
return new Container(app.channelUid,true)
|
|
||||||
}
|
|
||||||
let installPrompt = null
|
let installPrompt = null
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
//e.preventDefault();
|
//e.preventDefault();
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<section class="chat-area">
|
<section class="chat-area">
|
||||||
<div id="terminal" class="hidden"></div>
|
|
||||||
<message-list class="chat-messages">
|
<message-list class="chat-messages">
|
||||||
{% if not messages %}
|
{% if not messages %}
|
||||||
|
|
||||||
@ -43,22 +42,14 @@ const messagesContainer = document.querySelector(".chat-messages");
|
|||||||
const chatArea = document.querySelector(".chat-area");
|
const chatArea = document.querySelector(".chat-area");
|
||||||
const channelUid = "{{ channel.uid.value }}";
|
const channelUid = "{{ channel.uid.value }}";
|
||||||
const username = "{{ user.username.value }}";
|
const username = "{{ user.username.value }}";
|
||||||
let container = null
|
|
||||||
// --- Command completions ---
|
// --- Command completions ---
|
||||||
chatInputField.autoCompletions = {
|
chatInputField.autoCompletions = {
|
||||||
"/online": showOnline,
|
"/online": showOnline,
|
||||||
"/clear": () => { messagesContainer.innerHTML = ''; },
|
"/clear": () => { messagesContainer.innerHTML = ''; },
|
||||||
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
||||||
"/help": showHelp,
|
"/help": showHelp,
|
||||||
"/container": async() =>{
|
"/container": () =>{ containerDialog.openWithStatus()}
|
||||||
if(container == null){
|
|
||||||
container = await window.getContainer()
|
|
||||||
const terminal = document.querySelector("#terminal")
|
|
||||||
terminal.classList.toggle("hidden")
|
|
||||||
container.render(terminal)
|
|
||||||
}
|
|
||||||
containerDialog.openWithStatus()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Throttle utility ---
|
// --- Throttle utility ---
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
from snek.system.view import BaseView
|
|
||||||
import functools
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
class ContainerView(BaseView):
|
|
||||||
|
|
||||||
|
|
||||||
async def stdout_event_handler(self, ws, data):
|
|
||||||
try:
|
|
||||||
await ws.send_bytes(data)
|
|
||||||
except Exception as ex:
|
|
||||||
print(ex)
|
|
||||||
await ws.close()
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def create_stdout_event_handler(self, ws):
|
|
||||||
return functools.partial(self.stdout_event_handler, ws)
|
|
||||||
|
|
||||||
async def get(self):
|
|
||||||
|
|
||||||
ws = web.WebSocketResponse()
|
|
||||||
await ws.prepare(self.request)
|
|
||||||
if not self.request.session.get("logged_in"):
|
|
||||||
return self.HTTPUnauthorized()
|
|
||||||
|
|
||||||
channel_uid = self.request.match_info.get("channel_uid")
|
|
||||||
channel_member = await self.services.channel_member.get(channel_uid=channel_uid, user_uid=self.request.session.get("uid"))
|
|
||||||
if not channel_member:
|
|
||||||
return web.HTTPUnauthorized()
|
|
||||||
|
|
||||||
container = await self.services.container.get(channel_uid)
|
|
||||||
if not container:
|
|
||||||
return web.HTTPNotFound()
|
|
||||||
|
|
||||||
if not container['status'] == 'running':
|
|
||||||
resp = await self.services.container.start(channel_uid)
|
|
||||||
await ws.send_str(str(resp))
|
|
||||||
|
|
||||||
container_name = await self.services.container.get_container_name(channel_uid)
|
|
||||||
|
|
||||||
event_handler = await self.create_stdout_event_handler(ws)
|
|
||||||
|
|
||||||
await self.services.container.add_event_listener(container_name, "stdout", event_handler)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
data = await ws.receive()
|
|
||||||
if data.type == web.WSMsgType.TEXT:
|
|
||||||
|
|
||||||
await self.services.container.write_stdin(container_name, data.data)
|
|
||||||
elif data.type == web.WSMsgType.CLOSE:
|
|
||||||
break
|
|
||||||
elif data.type == web.WSMsgType.ERROR:
|
|
||||||
break
|
|
||||||
|
|
||||||
await self.services.container.remove_event_listener(container_name, channel_uid, "stdout")
|
|
||||||
|
|
||||||
return ws
|
|
||||||
|
|
||||||
|
|
||||||
@ -223,24 +223,6 @@ class RPCView(BaseView):
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def start_container(self, channel_uid):
|
|
||||||
self._require_login()
|
|
||||||
channel_member = await self.services.channel_member.get(
|
|
||||||
channel_uid=channel_uid, user_uid=self.user_uid
|
|
||||||
)
|
|
||||||
if not channel_member:
|
|
||||||
raise Exception("Not allowed")
|
|
||||||
return await self.services.container.start(channel_uid)
|
|
||||||
|
|
||||||
async def stop_container(self, channel_uid):
|
|
||||||
self._require_login()
|
|
||||||
channel_member = await self.services.channel_member.get(
|
|
||||||
channel_uid=channel_uid, user_uid=self.user_uid
|
|
||||||
)
|
|
||||||
if not channel_member:
|
|
||||||
raise Exception("Not allowed")
|
|
||||||
return await self.services.container.stop(channel_uid)
|
|
||||||
|
|
||||||
async def get_container_status(self, channel_uid):
|
async def get_container_status(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
channel_member = await self.services.channel_member.get(
|
channel_member = await self.services.channel_member.get(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user