Implement global queue, improve failure resiliency
This commit is contained in:
parent
ace50a79e7
commit
8f7cb14305
@ -4,15 +4,16 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
from vmmgr import VMMgr
|
from vmmgr import VMMgr
|
||||||
|
from vmmgr.config import Config
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='VM application manager')
|
parser = argparse.ArgumentParser(description='VM application manager')
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
parser_register_app = subparsers.add_parser('register-app')
|
parser_register_app = subparsers.add_parser('register-app')
|
||||||
parser_register_app.set_defaults(action='register-app')
|
parser_register_app.set_defaults(action='register-app')
|
||||||
parser_register_app.add_argument('app', help='Application name')
|
parser_register_app.add_argument('app')
|
||||||
parser_register_app.add_argument('login', help='Administrative login')
|
parser_register_app.add_argument('login', nargs='?')
|
||||||
parser_register_app.add_argument('password', help='Administrative password')
|
parser_register_app.add_argument('password', nargs='?')
|
||||||
|
|
||||||
parser_rebuild_issue = subparsers.add_parser('rebuild-issue')
|
parser_rebuild_issue = subparsers.add_parser('rebuild-issue')
|
||||||
parser_rebuild_issue.set_defaults(action='rebuild-issue')
|
parser_rebuild_issue.set_defaults(action='rebuild-issue')
|
||||||
@ -38,7 +39,8 @@ parser_unregister_proxy.set_defaults(action='unregister-proxy')
|
|||||||
parser_unregister_proxy.add_argument('app', help='Application name')
|
parser_unregister_proxy.add_argument('app', help='Application name')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
mgr = VMMgr()
|
conf = Config()
|
||||||
|
mgr = VMMgr(conf)
|
||||||
if args.action == 'register-app':
|
if args.action == 'register-app':
|
||||||
# Used by app install scripts
|
# Used by app install scripts
|
||||||
mgr.register_app(args.app, args.login, args.password)
|
mgr.register_app(args.app, args.login, args.password)
|
||||||
|
@ -78,6 +78,29 @@ server {{
|
|||||||
return 200 "vm-pong";
|
return 200 "vm-pong";
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen [::]:{port} ssl http2;
|
||||||
|
server_name ~^(.*)\.{domain_esc}$;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
return 503;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /static {{
|
||||||
|
root /usr/share/vmmgr;
|
||||||
|
}}
|
||||||
|
|
||||||
|
error_page 503 /503.html;
|
||||||
|
location = /503.html {{
|
||||||
|
root /usr/share/vmmgr/templates;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location = /vm-ping {{
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "vm-pong";
|
||||||
|
}}
|
||||||
|
}}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
ISSUE_TEMPLATE = '''
|
ISSUE_TEMPLATE = '''
|
||||||
@ -121,90 +144,21 @@ subjectAltName=DNS:{domain},DNS:*.{domain}"
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
class VMMgr:
|
class VMMgr:
|
||||||
def __init__(self):
|
def __init__(self, conf):
|
||||||
# Load JSON configuration
|
# Load JSON configuration
|
||||||
self.conf = Config()
|
self.conf = conf
|
||||||
self.domain = self.conf['host']['domain']
|
self.domain = conf['host']['domain']
|
||||||
self.port = self.conf['host']['port']
|
self.port = conf['host']['port']
|
||||||
|
|
||||||
def register_app(self, app, login, password):
|
def register_app(self, app, login, password):
|
||||||
# Register a newly installed application and update login and password
|
# Write a file with credentials of a newly installed application which
|
||||||
|
# will be picked up by thread performing the installation after the install script finishes
|
||||||
if app not in self.conf['packages']:
|
if app not in self.conf['packages']:
|
||||||
raise validator.InvalidValueException('app', app)
|
raise validator.InvalidValueException('app', app)
|
||||||
self.conf['apps'][app] = {
|
login = login if login else 'N/A'
|
||||||
'title': metadata['title'],
|
password = password if password else 'N/A'
|
||||||
'host': metadata['host'],
|
with open('/tmp/{}.credentials'.format(app), 'w') as f:
|
||||||
'login': login if login else 'N/A',
|
f.write('{}\n{}'.format(login, password))
|
||||||
'password': password if password else 'N/A',
|
|
||||||
'visible': False
|
|
||||||
}
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def show_tiles(self, app):
|
|
||||||
# Update visibility for the app in the configuration
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
self.conf['apps'][app]['visible'] = True
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def hide_tiles(self, app):
|
|
||||||
# Update visibility for the app in the configuration
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
self.conf['apps'][app]['visible'] = False
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def start_app(self, app):
|
|
||||||
# Start the actual app service
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
tools.start_service(app)
|
|
||||||
|
|
||||||
def stop_app(self, app):
|
|
||||||
# Stop the actual app service
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
tools.stop_service(app)
|
|
||||||
# Stop the app service's dependencies if they are not used by another running app
|
|
||||||
deps = self.build_deps_tree()
|
|
||||||
for dep in self.get_app_deps(app):
|
|
||||||
if not any([tools.is_service_started(d) for d in deps[dep]]):
|
|
||||||
tools.stop_service(dep)
|
|
||||||
|
|
||||||
def build_deps_tree(self):
|
|
||||||
# Fisrt, build a dictionary of {app: [needs]}
|
|
||||||
needs = {}
|
|
||||||
for app in self.conf['apps']:
|
|
||||||
needs[app] = self.get_app_deps(app)
|
|
||||||
# Then reverse it to {need: [apps]}
|
|
||||||
deps = {}
|
|
||||||
for app, need in needs.items():
|
|
||||||
for n in need:
|
|
||||||
deps.setdefault(n, []).append(app)
|
|
||||||
return deps
|
|
||||||
|
|
||||||
def get_app_deps(self, app):
|
|
||||||
# Get "needs" line from init script and split it to list
|
|
||||||
try:
|
|
||||||
with open(os.path.join('/etc/init.d', app), 'r') as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
if line.strip().startswith('need'):
|
|
||||||
return line.split()[1:]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return []
|
|
||||||
|
|
||||||
def enable_autostart(self, app):
|
|
||||||
# Add the app to OpenRC default runlevel
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
subprocess.run(['/sbin/rc-update', 'add', app])
|
|
||||||
|
|
||||||
def disable_autostart(self, app):
|
|
||||||
# Remove the app from OpenRC default runlevel
|
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
subprocess.run(['/sbin/rc-update', 'del', app])
|
|
||||||
|
|
||||||
def prepare_container(self):
|
def prepare_container(self):
|
||||||
# Extract the variables from values given via lxc.hook.pre-start hook
|
# Extract the variables from values given via lxc.hook.pre-start hook
|
||||||
@ -242,16 +196,12 @@ class VMMgr:
|
|||||||
|
|
||||||
def register_proxy(self, app):
|
def register_proxy(self, app):
|
||||||
# Setup proxy configuration and reload nginx
|
# Setup proxy configuration and reload nginx
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
||||||
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], domain=self.domain, port=self.port))
|
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['packages'][app]['host'], domain=self.domain, port=self.port))
|
||||||
tools.reload_nginx()
|
tools.reload_nginx()
|
||||||
|
|
||||||
def unregister_proxy(self, app):
|
def unregister_proxy(self, app):
|
||||||
# Remove proxy configuration and reload nginx
|
# Remove proxy configuration and reload nginx
|
||||||
if app not in self.conf['apps']:
|
|
||||||
raise validator.InvalidValueException('app', app)
|
|
||||||
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
||||||
tools.reload_nginx()
|
tools.reload_nginx()
|
||||||
|
|
||||||
@ -265,7 +215,7 @@ class VMMgr:
|
|||||||
self.port = self.conf['host']['port'] = port
|
self.port = self.conf['host']['port'] = port
|
||||||
self.conf.save()
|
self.conf.save()
|
||||||
# Restart all apps to trigger configuration refresh
|
# Restart all apps to trigger configuration refresh
|
||||||
for app in self.conf['apps']:
|
for app in self.conf['apps'].copy():
|
||||||
if tools.is_service_started(app):
|
if tools.is_service_started(app):
|
||||||
tools.restart_service(app)
|
tools.restart_service(app)
|
||||||
# Rebuild and restart nginx if it was requested.
|
# Rebuild and restart nginx if it was requested.
|
||||||
@ -274,7 +224,7 @@ class VMMgr:
|
|||||||
def rebuild_nginx(self):
|
def rebuild_nginx(self):
|
||||||
# Rebuild nginx config for the portal app. Web interface calls tools.restart_nginx() in WSGI close handler
|
# Rebuild nginx config for the portal app. Web interface calls tools.restart_nginx() in WSGI close handler
|
||||||
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
|
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
|
||||||
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
|
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port, domain_esc=self.domain.replace('.', '\.')))
|
||||||
|
|
||||||
def rebuild_issue(self):
|
def rebuild_issue(self):
|
||||||
# Compile the URLs displayed in terminal banner
|
# Compile the URLs displayed in terminal banner
|
||||||
@ -287,23 +237,6 @@ class VMMgr:
|
|||||||
with open(ISSUE_FILE, 'w') as f:
|
with open(ISSUE_FILE, 'w') as f:
|
||||||
f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(self.domain, self.port), ip=tools.compile_url(ip, self.port)))
|
f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(self.domain, self.port), ip=tools.compile_url(ip, self.port)))
|
||||||
|
|
||||||
def update_common(self, email, gmaps_api_key):
|
|
||||||
# Update common configuration values
|
|
||||||
if email != None:
|
|
||||||
# Update email
|
|
||||||
if not validator.is_valid_email(email):
|
|
||||||
raise validator.InvalidValueException('email', email)
|
|
||||||
self.conf['common']['email'] = email
|
|
||||||
if gmaps_api_key != None:
|
|
||||||
# Update Google Maps API key
|
|
||||||
self.conf['common']['gmaps-api-key'] = gmaps_api_key
|
|
||||||
# Save config to file
|
|
||||||
self.conf.save()
|
|
||||||
for app in self.conf['apps']:
|
|
||||||
# Restart currently running apps in order to update their config
|
|
||||||
if tools.is_service_started(app):
|
|
||||||
tools.restart_service(app)
|
|
||||||
|
|
||||||
def update_password(self, oldpassword, newpassword):
|
def update_password(self, oldpassword, newpassword):
|
||||||
# Update LUKS password and adminpwd for WSGI application
|
# Update LUKS password and adminpwd for WSGI application
|
||||||
input = '{}\n{}'.format(oldpassword, newpassword).encode()
|
input = '{}\n{}'.format(oldpassword, newpassword).encode()
|
||||||
@ -332,8 +265,8 @@ class VMMgr:
|
|||||||
# Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before
|
# Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before
|
||||||
if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)):
|
if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)):
|
||||||
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
|
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
|
||||||
for app in self.conf['apps']:
|
for app in self.conf['apps'].copy():
|
||||||
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
|
cmd += ['-d', '{}.{}'.format(self.conf['packages'][app]['host'], self.domain)]
|
||||||
cmd += ['-w', '/etc/acme.sh.d']
|
cmd += ['-w', '/etc/acme.sh.d']
|
||||||
# Request the certificate
|
# Request the certificate
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
|
69
usr/lib/python3.6/vmmgr/actionqueue.py
Normal file
69
usr/lib/python3.6/vmmgr/actionqueue.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
class ActionItem:
|
||||||
|
def __init__(self, key, action):
|
||||||
|
self.key = key
|
||||||
|
self.action = action
|
||||||
|
self.started = False
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
class ActionQueue:
|
||||||
|
def __init__(self):
|
||||||
|
self.actions = {}
|
||||||
|
# Priority 0 = restart/shutdown, 1 = config update, 2 = apps actions
|
||||||
|
self.queue = deque()
|
||||||
|
self.lock = Lock()
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
# Return copy of actions, so they can be traversed without state changes
|
||||||
|
with self.lock:
|
||||||
|
return self.actions.copy()
|
||||||
|
|
||||||
|
def enqueue_action(self, key, action):
|
||||||
|
# Enqueue action
|
||||||
|
with self.lock:
|
||||||
|
if key in self.actions:
|
||||||
|
# If the key alredy has a pending action, reject any other actions
|
||||||
|
return
|
||||||
|
item = ActionItem(key, action)
|
||||||
|
self.actions[key] = item
|
||||||
|
self.queue.append(item)
|
||||||
|
|
||||||
|
def process_actions(self):
|
||||||
|
# Main method for deferred queue processing called by WSGI close handler
|
||||||
|
with self.lock:
|
||||||
|
# If the queue is being processesd by another thread, allow this thread to be terminated
|
||||||
|
if self.is_running:
|
||||||
|
return
|
||||||
|
while True:
|
||||||
|
with self.lock:
|
||||||
|
# Try to get an item from queue
|
||||||
|
item = None
|
||||||
|
if self.queue:
|
||||||
|
item = self.queue.popleft()
|
||||||
|
# If there are no more queued items, unset the processing flag and allow the thread to be terminated
|
||||||
|
if not item:
|
||||||
|
self.is_running = False
|
||||||
|
return
|
||||||
|
# If there is an item to be processed, set processing flags and exit the lock
|
||||||
|
self.is_running = True
|
||||||
|
item.started = True
|
||||||
|
try:
|
||||||
|
# Call the method passed in item.action with the whole item as parameter
|
||||||
|
item.action(item)
|
||||||
|
# If the action finished without errors, restore nominal state by deleting the item from action list
|
||||||
|
self.clear_action(item.key)
|
||||||
|
except BaseException as e:
|
||||||
|
# If the action failed, store the exception and leave it in the list form manual clearance
|
||||||
|
with self.lock:
|
||||||
|
item.data = e
|
||||||
|
|
||||||
|
def clear_action(self, key):
|
||||||
|
# Restore nominal state by deleting the item from action list
|
||||||
|
with self.lock:
|
||||||
|
if key in self.actions:
|
||||||
|
del self.actions[key]
|
@ -6,30 +6,18 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||||
from threading import Lock
|
|
||||||
|
|
||||||
from . import tools
|
from . import tools
|
||||||
|
|
||||||
PUB_FILE = '/etc/vmmgr/packages.pub'
|
PUB_FILE = '/etc/vmmgr/packages.pub'
|
||||||
LXC_ROOT = '/var/lib/lxc'
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
|
|
||||||
class ActionItem:
|
|
||||||
def __init__(self, action, app):
|
|
||||||
self.timestamp = int(time.time())
|
|
||||||
self.action = action
|
|
||||||
self.app = app
|
|
||||||
self.started = False
|
|
||||||
self.finished = False
|
|
||||||
self.data = None
|
|
||||||
|
|
||||||
class InstallItem:
|
class InstallItem:
|
||||||
def __init__(self, total):
|
def __init__(self, total):
|
||||||
# Stage 0 = download, 1 = deps install, 2 = app install
|
# Stage 0 = download, 1 = deps install, 2 = app install
|
||||||
@ -42,12 +30,9 @@ class InstallItem:
|
|||||||
return str(min(99, round(self.downloaded / self.total * 100)))
|
return str(min(99, round(self.downloaded / self.total * 100)))
|
||||||
|
|
||||||
class AppMgr:
|
class AppMgr:
|
||||||
def __init__(self, vmmgr):
|
def __init__(self, conf):
|
||||||
self.vmmgr = vmmgr
|
self.conf = conf
|
||||||
self.conf = vmmgr.conf
|
|
||||||
self.online_packages = {}
|
self.online_packages = {}
|
||||||
self.action_queue = {}
|
|
||||||
self.lock = Lock()
|
|
||||||
|
|
||||||
def get_repo_resource(self, url, stream=False):
|
def get_repo_resource(self, url, stream=False):
|
||||||
return requests.get('{}/{}'.format(self.conf['repo']['url'], url), auth=(self.conf['repo']['user'], self.conf['repo']['pwd']), timeout=5, stream=stream)
|
return requests.get('{}/{}'.format(self.conf['repo']['url'], url), auth=(self.conf['repo']['user'], self.conf['repo']['pwd']), timeout=5, stream=stream)
|
||||||
@ -64,48 +49,47 @@ class AppMgr:
|
|||||||
# Minimze the time when self.online_packages is out of sync
|
# Minimze the time when self.online_packages is out of sync
|
||||||
self.online_packages = online_packages
|
self.online_packages = online_packages
|
||||||
|
|
||||||
def enqueue_action(self, action, app):
|
|
||||||
# Remove actions older than 1 day
|
|
||||||
for id,item in self.action_queue.items():
|
|
||||||
if item.timestamp < time.time() - 86400:
|
|
||||||
del self.item[id]
|
|
||||||
# Enqueue action
|
|
||||||
id = '{}:{}'.format(app, uuid.uuid4())
|
|
||||||
item = ActionItem(action, app)
|
|
||||||
self.action_queue[id] = item
|
|
||||||
return id,item
|
|
||||||
|
|
||||||
def get_actions(self, ids):
|
|
||||||
# Return list of requested actions
|
|
||||||
result = {}
|
|
||||||
for id in ids:
|
|
||||||
result[id] = self.action_queue[id] if id in self.action_queue else None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def process_action(self, id):
|
|
||||||
# Main method for deferred queue processing called by WSGI close handler
|
|
||||||
item = self.action_queue[id]
|
|
||||||
with self.lock:
|
|
||||||
item.started = True
|
|
||||||
try:
|
|
||||||
# Call the action method inside exclusive lock
|
|
||||||
getattr(self, item.action)(item)
|
|
||||||
except BaseException as e:
|
|
||||||
item.data = e
|
|
||||||
finally:
|
|
||||||
item.finished = True
|
|
||||||
|
|
||||||
def start_app(self, item):
|
def start_app(self, item):
|
||||||
if not tools.is_service_started(item.app):
|
# Start the actual app service
|
||||||
self.vmmgr.start_app(item.app)
|
app = item.key
|
||||||
|
if app not in self.conf['apps']:
|
||||||
|
raise validator.InvalidValueException('app', app)
|
||||||
|
if not tools.is_service_started(app):
|
||||||
|
tools.start_service(app)
|
||||||
|
|
||||||
def stop_app(self, item):
|
def stop_app(self, item):
|
||||||
if tools.is_service_started(item.app):
|
# Stop the actual app service
|
||||||
self.vmmgr.stop_app(item.app)
|
app = item.key
|
||||||
|
if app not in self.conf['apps']:
|
||||||
|
raise validator.InvalidValueException('app', app)
|
||||||
|
if tools.is_service_started(app):
|
||||||
|
tools.stop_service(app)
|
||||||
|
# Stop the app service's dependencies if they are not used by another running app
|
||||||
|
deps = self.get_services_deps()
|
||||||
|
for dep in tools.get_service_deps(app):
|
||||||
|
if not any([tools.is_service_started(d) for d in deps[dep]]):
|
||||||
|
tools.stop_service(dep)
|
||||||
|
|
||||||
|
def update_app_visibility(self, app, visible):
|
||||||
|
# Update visibility for the app in the configuration
|
||||||
|
if app not in self.conf['apps']:
|
||||||
|
raise validator.InvalidValueException('app', app)
|
||||||
|
self.conf['apps'][app]['visible'] = visible
|
||||||
|
self.conf.save()
|
||||||
|
|
||||||
|
def update_app_autostart(self, app, enabled):
|
||||||
|
# Add/remove the app to OpenRC default runlevel
|
||||||
|
if app not in self.conf['apps']:
|
||||||
|
raise validator.InvalidValueException('app', app)
|
||||||
|
subprocess.run(['/sbin/rc-update', 'add' if enabled else 'del', app])
|
||||||
|
|
||||||
def install_app(self, item):
|
def install_app(self, item):
|
||||||
# Main installation function. Wrapper for download, registration and install script
|
# Main installation function. Wrapper for download, registration and install script
|
||||||
deps = [d for d in self.get_install_deps(item.app) if d not in self.conf['packages']]
|
app = item.key
|
||||||
|
# Clean packages which previously failed to install
|
||||||
|
self.clean_pending_packages()
|
||||||
|
# Get all packages on which the app depends and which have not been installed yet
|
||||||
|
deps = [d for d in self.get_install_deps(app) if d not in self.conf['packages'] or 'pending' in self.conf['packages'][d]]
|
||||||
item.data = InstallItem(sum(self.online_packages[d]['size'] for d in deps))
|
item.data = InstallItem(sum(self.online_packages[d]['size'] for d in deps))
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
self.download_package(dep, item.data)
|
self.download_package(dep, item.data)
|
||||||
@ -118,13 +102,15 @@ class AppMgr:
|
|||||||
self.run_uninstall_script(dep)
|
self.run_uninstall_script(dep)
|
||||||
self.register_package(dep)
|
self.register_package(dep)
|
||||||
self.run_install_script(dep)
|
self.run_install_script(dep)
|
||||||
|
self.finalize_installation(dep)
|
||||||
|
|
||||||
def uninstall_app(self, item):
|
def uninstall_app(self, item):
|
||||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
||||||
|
app = item.key
|
||||||
self.stop_app(item)
|
self.stop_app(item)
|
||||||
if tools.is_service_autostarted(item.app):
|
if tools.is_service_autostarted(app):
|
||||||
self.vmmgr.disable_autostart(item.app)
|
self.update_app_autostart(app, False)
|
||||||
deps = self.get_install_deps(item.app, False)[::-1]
|
deps = self.get_install_deps(app, False)[::-1]
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
if dep not in self.get_uninstall_deps():
|
if dep not in self.get_uninstall_deps():
|
||||||
self.run_uninstall_script(dep)
|
self.run_uninstall_script(dep)
|
||||||
@ -138,13 +124,13 @@ class AppMgr:
|
|||||||
for chunk in r.iter_content(chunk_size=65536):
|
for chunk in r.iter_content(chunk_size=65536):
|
||||||
if chunk:
|
if chunk:
|
||||||
installitem.downloaded += f.write(chunk)
|
installitem.downloaded += f.write(chunk)
|
||||||
|
|
||||||
def unpack_package(self, name):
|
|
||||||
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
|
||||||
# Verify hash
|
# Verify hash
|
||||||
if self.online_packages[name]['sha512'] != hash_file(tmp_archive):
|
if self.online_packages[name]['sha512'] != hash_file(tmp_archive):
|
||||||
raise InvalidSignature(name)
|
raise InvalidSignature(name)
|
||||||
# Unpack
|
|
||||||
|
def unpack_package(self, name):
|
||||||
|
# Unpack archive
|
||||||
|
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
||||||
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
|
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
|
||||||
os.unlink(tmp_archive)
|
os.unlink(tmp_archive)
|
||||||
|
|
||||||
@ -165,14 +151,40 @@ class AppMgr:
|
|||||||
# Registers a package in local configuration
|
# Registers a package in local configuration
|
||||||
metadata = self.online_packages[name].copy()
|
metadata = self.online_packages[name].copy()
|
||||||
del metadata['sha512']
|
del metadata['sha512']
|
||||||
|
del metadata['size']
|
||||||
|
metadata['pending'] = True
|
||||||
self.conf['packages'][name] = metadata
|
self.conf['packages'][name] = metadata
|
||||||
self.conf.save()
|
self.conf.save()
|
||||||
|
|
||||||
def unregister_package(self, name):
|
def unregister_package(self, name):
|
||||||
# Removes a package from local configuration
|
# Removes a package from local configuration
|
||||||
del self.conf['packages'][name]
|
|
||||||
if name in self.conf['apps']:
|
if name in self.conf['apps']:
|
||||||
del self.conf['apps'][name]
|
del self.conf['apps'][name]
|
||||||
|
del self.conf['packages'][name]
|
||||||
|
self.conf.save()
|
||||||
|
|
||||||
|
def finalize_installation(self, name):
|
||||||
|
# If the install script called vmmgr register-app, perform the app registration
|
||||||
|
# This can't be done directly from install script due to possible race conditions
|
||||||
|
cred_file = '/tmp/{}.credentials'.format(name)
|
||||||
|
if os.path.exists(cred_file):
|
||||||
|
with open(cred_file, 'r') as f:
|
||||||
|
cred = f.read().splitlines()
|
||||||
|
os.unlink(cred_file)
|
||||||
|
self.conf['apps'][name] = {
|
||||||
|
'login': cred[0],
|
||||||
|
'password': cred[1],
|
||||||
|
'visible': False
|
||||||
|
}
|
||||||
|
# Finally, mark the package as fully installed
|
||||||
|
del self.conf['packages'][name]['pending']
|
||||||
|
self.conf.save()
|
||||||
|
|
||||||
|
def clean_pending_packages(self):
|
||||||
|
# Remove registeres packages with pending flag set from previously failed installation
|
||||||
|
for name in self.conf['packages'].copy():
|
||||||
|
if 'pending' in self.conf['packages'][name]:
|
||||||
|
self.unregister_package(name)
|
||||||
self.conf.save()
|
self.conf.save()
|
||||||
|
|
||||||
def run_install_script(self, name):
|
def run_install_script(self, name):
|
||||||
@ -203,11 +215,40 @@ class AppMgr:
|
|||||||
def get_uninstall_deps(self):
|
def get_uninstall_deps(self):
|
||||||
# Create reverse dependency tree for all installed packages
|
# Create reverse dependency tree for all installed packages
|
||||||
deps = {}
|
deps = {}
|
||||||
for pkg in self.conf['packages']:
|
for name in self.conf['packages'].copy():
|
||||||
for d in self.conf['packages'][pkg]['deps']:
|
for d in self.conf['packages'][name]['deps']:
|
||||||
deps.setdefault(d, []).append(pkg)
|
deps.setdefault(d, []).append(name)
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
|
def get_services_deps(self):
|
||||||
|
# Fisrt, build a dictionary of {app: [needs]}
|
||||||
|
needs = {}
|
||||||
|
for app in self.conf['apps'].copy():
|
||||||
|
needs[app] = tools.get_service_deps(app)
|
||||||
|
# Then reverse it to {need: [apps]}
|
||||||
|
deps = {}
|
||||||
|
for app, need in needs.items():
|
||||||
|
for n in need:
|
||||||
|
deps.setdefault(n, []).append(app)
|
||||||
|
return deps
|
||||||
|
|
||||||
|
def update_common_settings(self, email, gmaps_api_key):
|
||||||
|
# Update common configuration values
|
||||||
|
if email != None:
|
||||||
|
# Update email
|
||||||
|
if not validator.is_valid_email(email):
|
||||||
|
raise validator.InvalidValueException('email', email)
|
||||||
|
self.conf['common']['email'] = email
|
||||||
|
if gmaps_api_key != None:
|
||||||
|
# Update Google Maps API key
|
||||||
|
self.conf['common']['gmaps-api-key'] = gmaps_api_key
|
||||||
|
# Save config to file
|
||||||
|
self.conf.save()
|
||||||
|
for app in self.conf['apps'].copy():
|
||||||
|
# Restart currently running apps in order to update their config
|
||||||
|
if tools.is_service_started(app):
|
||||||
|
tools.restart_service(app)
|
||||||
|
|
||||||
def hash_file(file_path):
|
def hash_file(file_path):
|
||||||
sha512 = hashlib.sha512()
|
sha512 = hashlib.sha512()
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
|
@ -4,6 +4,7 @@ import fcntl
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
CONF_FILE = '/etc/vmmgr/config.json'
|
CONF_FILE = '/etc/vmmgr/config.json'
|
||||||
|
LOCK_FILE = '/var/lock/vmmgr-config.lock'
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -11,14 +12,14 @@ class Config:
|
|||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
# Load configuration from file. Uses file lock as interprocess mutex
|
# Load configuration from file. Uses file lock as interprocess mutex
|
||||||
with open('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
with open(LOCK_FILE, 'w') as lock:
|
||||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||||
with open(CONF_FILE, 'r') as f:
|
with open(CONF_FILE, 'r') as f:
|
||||||
self.data = json.load(f)
|
self.data = json.load(f)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
# Save configuration to a file. Uses file lock as interprocess mutex
|
# Save configuration to a file. Uses file lock as interprocess mutex
|
||||||
with open('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
with open(LOCK_FILE, 'w') as lock:
|
||||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||||
with open(CONF_FILE, 'w') as f:
|
with open(CONF_FILE, 'w') as f:
|
||||||
json.dump(self.data, f, sort_keys=True, indent=4)
|
json.dump(self.data, f, sort_keys=True, indent=4)
|
||||||
|
@ -22,6 +22,7 @@ def get_local_ip(version):
|
|||||||
# Return first routable IPv4/6 address of the VM (container host)
|
# Return first routable IPv4/6 address of the VM (container host)
|
||||||
try:
|
try:
|
||||||
output = subprocess.run(['/sbin/ip', 'route', 'get', '1' if version == 4 else '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()
|
output = subprocess.run(['/sbin/ip', 'route', 'get', '1' if version == 4 else '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()
|
||||||
|
# Get field right after 'src'
|
||||||
return output[output.index('src')+1]
|
return output[output.index('src')+1]
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@ -60,6 +61,17 @@ def ping_url(url):
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_service_deps(app):
|
||||||
|
# Get "needs" line from init script and split it to list
|
||||||
|
try:
|
||||||
|
with open(os.path.join('/etc/init.d', app), 'r') as f:
|
||||||
|
for line in f.readlines():
|
||||||
|
if line.strip().startswith('need'):
|
||||||
|
return line.split()[1:]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
def is_service_started(app):
|
def is_service_started(app):
|
||||||
# Check OpenRC service status without calling any binary
|
# Check OpenRC service status without calling any binary
|
||||||
return os.path.exists(os.path.join('/run/openrc/started', app))
|
return os.path.exists(os.path.join('/run/openrc/started', app))
|
||||||
@ -117,17 +129,21 @@ def update_hosts_lease(app, is_request):
|
|||||||
ip = None
|
ip = None
|
||||||
with open('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
with open('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
||||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||||
|
# Load all existing records
|
||||||
with open('/etc/hosts', 'r') as f:
|
with open('/etc/hosts', 'r') as f:
|
||||||
leases = [l.strip().split(' ', 1) for l in f]
|
leases = [l.strip().split(' ', 1) for l in f]
|
||||||
|
# If this call is a request for lease, find the first unassigned IP
|
||||||
if is_request:
|
if is_request:
|
||||||
used_ips = [l[0] lor l in leases]
|
used_ips = [l[0] for l in leases]
|
||||||
for i in range(2, 65534):
|
for i in range(2, 65534):
|
||||||
ip = '172.17.{}.{}'. format(i // 256, i % 256)
|
ip = '172.17.{}.{}'. format(i // 256, i % 256)
|
||||||
if ip not in used_ips:
|
if ip not in used_ips:
|
||||||
leases.append([ip, app])
|
leases.append([ip, app])
|
||||||
break
|
break
|
||||||
|
# Otherwise it is a release in which case we just delete the record
|
||||||
else:
|
else:
|
||||||
leases = [l for l in leases if l[1] != app]
|
leases = [l for l in leases if l[1] != app]
|
||||||
|
# Write the contents back to the file
|
||||||
with open('/etc/hosts', 'w') as f:
|
with open('/etc/hosts', 'w') as f:
|
||||||
for lease in leases:
|
for lease in leases:
|
||||||
f.write('{} {}\n'.format(lease[0], lease[1]))
|
f.write('{} {}\n'.format(lease[0], lease[1]))
|
||||||
|
@ -12,7 +12,9 @@ from jinja2 import Environment, FileSystemLoader
|
|||||||
|
|
||||||
from . import VMMgr, CERT_PUB_FILE
|
from . import VMMgr, CERT_PUB_FILE
|
||||||
from . import tools
|
from . import tools
|
||||||
|
from .actionqueue import ActionQueue
|
||||||
from .appmgr import AppMgr
|
from .appmgr import AppMgr
|
||||||
|
from .config import Config
|
||||||
from .validator import InvalidValueException
|
from .validator import InvalidValueException
|
||||||
from .wsgilang import WSGILang
|
from .wsgilang import WSGILang
|
||||||
from .wsgisession import WSGISession
|
from .wsgisession import WSGISession
|
||||||
@ -21,21 +23,20 @@ SESSION_KEY = os.urandom(26)
|
|||||||
|
|
||||||
class WSGIApp(object):
|
class WSGIApp(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.vmmgr = VMMgr()
|
self.conf = Config()
|
||||||
self.appmgr = AppMgr(self.vmmgr)
|
self.vmmgr = VMMgr(self.conf)
|
||||||
self.conf = self.vmmgr.conf
|
self.appmgr = AppMgr(self.conf)
|
||||||
|
self.queue = ActionQueue()
|
||||||
|
# Clean broken and interrupted installations in case of unclean previous shutdown
|
||||||
|
self.appmgr.clean_pending_packages()
|
||||||
self.jinja_env = Environment(loader=FileSystemLoader('/usr/share/vmmgr/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True)
|
self.jinja_env = Environment(loader=FileSystemLoader('/usr/share/vmmgr/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True)
|
||||||
self.jinja_env.globals.update(is_app_visible=self.is_app_visible)
|
self.jinja_env.globals.update(is_app_visible=self.is_app_visible)
|
||||||
self.jinja_env.globals.update(is_service_autostarted=tools.is_service_autostarted)
|
|
||||||
self.jinja_env.globals.update(is_service_started=tools.is_service_started)
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
return self.wsgi_app(environ, start_response)
|
return self.wsgi_app(environ, start_response)
|
||||||
|
|
||||||
def wsgi_app(self, environ, start_response):
|
def wsgi_app(self, environ, start_response):
|
||||||
request = Request(environ)
|
request = Request(environ)
|
||||||
# Reload config in case it has changed between requests
|
|
||||||
self.conf.load()
|
|
||||||
# Enhance request
|
# Enhance request
|
||||||
request.session = WSGISession(request.cookies, SESSION_KEY)
|
request.session = WSGISession(request.cookies, SESSION_KEY)
|
||||||
request.session.lang = WSGILang()
|
request.session.lang = WSGILang()
|
||||||
@ -52,7 +53,7 @@ class WSGIApp(object):
|
|||||||
return getattr(self, endpoint)(request, **values)
|
return getattr(self, endpoint)(request, **values)
|
||||||
except NotFound as e:
|
except NotFound as e:
|
||||||
# Return custom 404 page
|
# Return custom 404 page
|
||||||
response = self.render_template('404.html', request)
|
response = self.render_html('404.html', request)
|
||||||
response.status_code = 404
|
response.status_code = 404
|
||||||
return response
|
return response
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
@ -81,7 +82,8 @@ class WSGIApp(object):
|
|||||||
Rule('/start-app', endpoint='start_app_action'),
|
Rule('/start-app', endpoint='start_app_action'),
|
||||||
Rule('/stop-app', endpoint='stop_app_action'),
|
Rule('/stop-app', endpoint='stop_app_action'),
|
||||||
Rule('/install-app', endpoint='install_app_action'),
|
Rule('/install-app', endpoint='install_app_action'),
|
||||||
Rule('/get-progress', endpoint='get_progress_action'),
|
Rule('/get-app-status', endpoint='get_app_status_action'),
|
||||||
|
Rule('/clear-app-status', endpoint='clear_app_status_action'),
|
||||||
Rule('/uninstall-app', endpoint='uninstall_app_action'),
|
Rule('/uninstall-app', endpoint='uninstall_app_action'),
|
||||||
Rule('/update-password', endpoint='update_password_action'),
|
Rule('/update-password', endpoint='update_password_action'),
|
||||||
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
|
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
|
||||||
@ -98,15 +100,20 @@ class WSGIApp(object):
|
|||||||
# Enhance context
|
# Enhance context
|
||||||
context['conf'] = self.conf
|
context['conf'] = self.conf
|
||||||
context['session'] = request.session
|
context['session'] = request.session
|
||||||
|
context['lang'] = request.session.lang
|
||||||
# Render template
|
# Render template
|
||||||
t = self.jinja_env.get_template(template_name)
|
template = self.jinja_env.get_template(template_name)
|
||||||
return Response(t.render(context), mimetype='text/html')
|
return template.render(context)
|
||||||
|
|
||||||
|
def render_html(self, template_name, request, **context):
|
||||||
|
html = self.render_template(template_name, request, **context)
|
||||||
|
return Response(html, mimetype='text/html')
|
||||||
|
|
||||||
def render_json(self, data):
|
def render_json(self, data):
|
||||||
return Response(json.dumps(data), mimetype='application/json')
|
return Response(json.dumps(data), mimetype='application/json')
|
||||||
|
|
||||||
def login_view(self, request, **kwargs):
|
def login_view(self, request, **kwargs):
|
||||||
return self.render_template('login.html', request, redirect=kwargs['redirect'])
|
return self.render_html('login.html', request, redirect=kwargs['redirect'])
|
||||||
|
|
||||||
def login_action(self, request):
|
def login_action(self, request):
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
@ -115,7 +122,7 @@ class WSGIApp(object):
|
|||||||
request.session['admin'] = True
|
request.session['admin'] = True
|
||||||
return redirect(redir_url)
|
return redirect(redir_url)
|
||||||
else:
|
else:
|
||||||
return self.render_template('login.html', request, message=request.session.lang.bad_password())
|
return self.render_html('login.html', request, message=request.session.lang.bad_password())
|
||||||
|
|
||||||
def logout_action(self, request):
|
def logout_action(self, request):
|
||||||
request.session.reset()
|
request.session.reset()
|
||||||
@ -125,8 +132,8 @@ class WSGIApp(object):
|
|||||||
# Default portal view.
|
# Default portal view.
|
||||||
host = tools.compile_url(self.conf['host']['domain'], self.conf['host']['port'])[8:]
|
host = tools.compile_url(self.conf['host']['domain'], self.conf['host']['port'])[8:]
|
||||||
if request.session['admin']:
|
if request.session['admin']:
|
||||||
return self.render_template('portal-admin.html', request, host=host)
|
return self.render_html('portal-admin.html', request, host=host)
|
||||||
return self.render_template('portal-user.html', request, host=host)
|
return self.render_html('portal-user.html', request, host=host)
|
||||||
|
|
||||||
def setup_host_view(self, request):
|
def setup_host_view(self, request):
|
||||||
# Host setup view.
|
# Host setup view.
|
||||||
@ -135,7 +142,7 @@ class WSGIApp(object):
|
|||||||
in_ipv4 = tools.get_local_ip(4)
|
in_ipv4 = tools.get_local_ip(4)
|
||||||
in_ipv6 = tools.get_local_ip(6)
|
in_ipv6 = tools.get_local_ip(6)
|
||||||
cert_info = tools.get_cert_info(CERT_PUB_FILE)
|
cert_info = tools.get_cert_info(CERT_PUB_FILE)
|
||||||
return self.render_template('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info)
|
return self.render_html('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info)
|
||||||
|
|
||||||
def setup_apps_view(self, request):
|
def setup_apps_view(self, request):
|
||||||
# Application manager view.
|
# Application manager view.
|
||||||
@ -143,60 +150,72 @@ class WSGIApp(object):
|
|||||||
self.appmgr.fetch_online_packages()
|
self.appmgr.fetch_online_packages()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
all_apps = sorted(set([k for k,v in self.appmgr.online_packages.items() if 'host' in v] + list(self.conf['apps'].keys())))
|
repo_reachable = bool(self.appmgr.online_packages)
|
||||||
return self.render_template('setup-apps.html', request, all_apps=all_apps, online_packages=self.appmgr.online_packages)
|
table = self.render_setup_apps_table(request)
|
||||||
|
return self.render_html('setup-apps.html', request, repo_reachable=repo_reachable, table=table)
|
||||||
|
|
||||||
def render_setup_apps_row(self, request, app, app_title, item):
|
def render_setup_apps_table(self, request):
|
||||||
lang = request.session.lang
|
lang = request.session.lang
|
||||||
actions = '<div class="loader"></div>'
|
pending_actions = self.queue.get_actions()
|
||||||
if item.action == 'start_app':
|
actionable_apps = sorted(set([k for k,v in self.appmgr.online_packages.items() if 'host' in v] + list(self.conf['apps'].keys())))
|
||||||
if not item.started:
|
app_data = {}
|
||||||
status = 'Spouští se (ve frontě)'
|
for app in actionable_apps:
|
||||||
elif not item.finished:
|
installed = app in self.conf['apps']
|
||||||
status = 'Spouští se'
|
title = self.conf['packages'][app]['title'] if installed else self.appmgr.online_packages[app]['title']
|
||||||
elif isinstance(item.data, BaseException):
|
visible = self.conf['apps'][app]['visible'] if installed else False
|
||||||
status = '<span class="error">{}</span>'.format(lang.stop_start_error())
|
autostarted = tools.is_service_autostarted(app) if installed else False
|
||||||
|
if app in pending_actions:
|
||||||
|
item = pending_actions[app]
|
||||||
|
actions = '<div class="loader"></div>'
|
||||||
|
if item.action == self.appmgr.start_app:
|
||||||
|
if not item.started:
|
||||||
|
status = '{} ({})'.format(lang.status_starting(), lang.status_queued())
|
||||||
|
elif isinstance(item.data, BaseException):
|
||||||
|
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.stop_start_error())
|
||||||
|
actions = None
|
||||||
|
else:
|
||||||
|
status = lang.status_starting()
|
||||||
|
elif item.action == self.appmgr.stop_app:
|
||||||
|
if not item.started:
|
||||||
|
status = '{} ({})'.format(lang.status_stopping(), lang.status_queued())
|
||||||
|
elif isinstance(item.data, BaseException):
|
||||||
|
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.stop_start_error())
|
||||||
|
actions = None
|
||||||
|
else:
|
||||||
|
status = lang.status_stopping()
|
||||||
|
elif item.action == self.appmgr.install_app:
|
||||||
|
if not item.started:
|
||||||
|
status = '{} ({})'.format(lang.status_downloading(), lang.status_queued())
|
||||||
|
elif isinstance(item.data, BaseException):
|
||||||
|
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.package_manager_error())
|
||||||
|
actions = None
|
||||||
|
else:
|
||||||
|
if item.data.stage == 0:
|
||||||
|
status = '{} ({} %)'.format(lang.status_downloading(), item.data)
|
||||||
|
elif item.data.stage == 1:
|
||||||
|
status = lang.status_installing_deps()
|
||||||
|
else:
|
||||||
|
status = lang.status_installing()
|
||||||
|
elif item.action == self.appmgr.uninstall_app:
|
||||||
|
if not item.started:
|
||||||
|
status = '{} ({})'.format(lang.status_uninstalling(), lang.status_queued())
|
||||||
|
elif isinstance(item.data, BaseException):
|
||||||
|
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.package_manager_error())
|
||||||
|
actions = None
|
||||||
|
else:
|
||||||
|
status = lang.status_uninstalling()
|
||||||
else:
|
else:
|
||||||
status = '<span class="info">Spuštěna</span>'
|
if not installed:
|
||||||
actions = '<a href="#" class="app-stop">Zastavit</a>'
|
status = lang.status_not_installed()
|
||||||
elif item.action == 'stop_app':
|
actions = '<a href="#" class="app-install">{}</a>'.format(lang.action_install())
|
||||||
if not item.started:
|
elif tools.is_service_started(app):
|
||||||
status = 'Zastavuje se (ve frontě)'
|
status = '<span class="info">{}</span>'.format(lang.status_started())
|
||||||
elif not item.finished:
|
actions = '<a href="#" class="app-stop">{}</a>'.format(lang.action_stop())
|
||||||
status = 'Zastavuje se'
|
|
||||||
elif isinstance(item.data, BaseException):
|
|
||||||
status = '<span class="error">{}</span>'.format(lang.stop_start_error())
|
|
||||||
else:
|
|
||||||
status = '<span class="error">Zastavena</span>'
|
|
||||||
actions = '<a href="#" class="app-start">Spustit</a>, <a href="#" class="app-uninstall">Odinstalovat</a>'
|
|
||||||
elif item.action == 'install_app':
|
|
||||||
if not item.started:
|
|
||||||
status = 'Stahuje se (ve frontě)'
|
|
||||||
elif not item.finished:
|
|
||||||
if item.data.stage == 0:
|
|
||||||
status = 'Stahuje se ({} %)'.format(item.data)
|
|
||||||
elif item.data.stage == 1:
|
|
||||||
status = 'Instalují se závislosti'
|
|
||||||
else:
|
else:
|
||||||
status = 'Instaluje se'
|
status = '<span class="error">{}</span>'.format(lang.status_stopped())
|
||||||
elif isinstance(item.data, BaseException):
|
actions = '<a href="#" class="app-start">{}</a>, <a href="#" class="app-uninstall">{}</a>'.format(lang.action_start(), lang.action_uninstall())
|
||||||
status = '<span class="error">{}</span>'.format(lang.package_manager_error())
|
app_data[app] = {'title': title, 'visible': visible, 'installed': installed,'autostarted': autostarted, 'status': status, 'actions': actions}
|
||||||
else:
|
return self.render_template('setup-apps-table.html', request, app_data=app_data)
|
||||||
status = '<span class="error">Zastavena</span>'
|
|
||||||
actions = '<a href="#" class="app-start">Spustit</a>, <a href="#" class="app-uninstall">Odinstalovat</a>'
|
|
||||||
elif item.action == 'uninstall_app':
|
|
||||||
if not item.started:
|
|
||||||
status = 'Odinstalovává se (ve frontě)'
|
|
||||||
elif not item.finished:
|
|
||||||
status = 'Odinstalovává se'
|
|
||||||
elif isinstance(item.data, BaseException):
|
|
||||||
status = '<span class="error">{}</span>'.format(lang.package_manager_error())
|
|
||||||
else:
|
|
||||||
status = 'Není nainstalována'
|
|
||||||
actions = '<a href="#" class="app-install">Instalovat</a>'
|
|
||||||
is_error = isinstance(item.data, BaseException)
|
|
||||||
t = self.jinja_env.get_template('setup-apps-row.html')
|
|
||||||
return t.render({'conf': self.conf, 'session': request.session, 'app': app, 'app_title': app_title, 'status': status, 'actions': actions, 'is_error': is_error})
|
|
||||||
|
|
||||||
def update_host_action(self, request):
|
def update_host_action(self, request):
|
||||||
# Update domain and port, then restart nginx
|
# Update domain and port, then restart nginx
|
||||||
@ -219,7 +238,7 @@ class WSGIApp(object):
|
|||||||
|
|
||||||
def verify_dns_action(self, request):
|
def verify_dns_action(self, request):
|
||||||
# Check if all FQDNs for all applications are resolvable and point to current external IP
|
# Check if all FQDNs for all applications are resolvable and point to current external IP
|
||||||
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
|
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['packages'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
|
||||||
ipv4 = tools.get_external_ip(4)
|
ipv4 = tools.get_external_ip(4)
|
||||||
ipv6 = tools.get_external_ip(6)
|
ipv6 = tools.get_external_ip(6)
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
@ -240,7 +259,7 @@ class WSGIApp(object):
|
|||||||
# Check if all applications are accessible from the internet using 3rd party ping service
|
# Check if all applications are accessible from the internet using 3rd party ping service
|
||||||
proto = kwargs['proto']
|
proto = kwargs['proto']
|
||||||
port = self.vmmgr.port if proto == 'https' else '80'
|
port = self.vmmgr.port if proto == 'https' else '80'
|
||||||
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
|
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['packages'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
url = tools.compile_url(domain, port, proto)
|
url = tools.compile_url(domain, port, proto)
|
||||||
try:
|
try:
|
||||||
@ -279,7 +298,7 @@ class WSGIApp(object):
|
|||||||
def update_common_action(self, request):
|
def update_common_action(self, request):
|
||||||
# Update common settings shared between apps - admin e-mail address, Google Maps API key
|
# Update common settings shared between apps - admin e-mail address, Google Maps API key
|
||||||
try:
|
try:
|
||||||
self.vmmgr.update_common(request.form['email'], request.form['gmaps-api-key'])
|
self.appmgr.update_common_settings(request.form['email'], request.form['gmaps-api-key'])
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
return self.render_json({'error': request.session.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
return self.render_json({'ok': request.session.lang.common_updated()})
|
return self.render_json({'ok': request.session.lang.common_updated()})
|
||||||
@ -298,10 +317,7 @@ class WSGIApp(object):
|
|||||||
def update_app_visibility_action(self, request):
|
def update_app_visibility_action(self, request):
|
||||||
# Update application visibility on portal page
|
# Update application visibility on portal page
|
||||||
try:
|
try:
|
||||||
if request.form['value'] == 'true':
|
self.appmgr.update_app_visibility(request.form['app'], request.form['value'] == 'true')
|
||||||
self.vmmgr.show_tiles(request.form['app'])
|
|
||||||
else:
|
|
||||||
self.vmmgr.hide_tiles(request.form['app'])
|
|
||||||
except (BadRequest, InvalidValueException):
|
except (BadRequest, InvalidValueException):
|
||||||
return self.render_json({'error': request.session.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
return self.render_json({'ok': 'ok'})
|
return self.render_json({'ok': 'ok'})
|
||||||
@ -309,55 +325,50 @@ class WSGIApp(object):
|
|||||||
def update_app_autostart_action(self, request):
|
def update_app_autostart_action(self, request):
|
||||||
# Update value determining if the app should be automatically started after VM boot
|
# Update value determining if the app should be automatically started after VM boot
|
||||||
try:
|
try:
|
||||||
if request.form['value'] == 'true':
|
self.appmgr.update_app_autostart(request.form['app'], request.form['value'] == 'true')
|
||||||
self.vmmgr.enable_autostart(request.form['app'])
|
|
||||||
else:
|
|
||||||
self.vmmgr.disable_autostart(request.form['app'])
|
|
||||||
except (BadRequest, InvalidValueException):
|
except (BadRequest, InvalidValueException):
|
||||||
return self.render_json({'error': request.session.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
return self.render_json({'ok': 'ok'})
|
return self.render_json({'ok': 'ok'})
|
||||||
|
|
||||||
def enqueue_action(self, request, action):
|
def enqueue_app_action(self, request, action):
|
||||||
|
# Common method for enqueuing app actions
|
||||||
try:
|
try:
|
||||||
app = request.form['app']
|
app = request.form['app']
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
return self.render_json({'error': request.session.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
app_title = self.conf['apps'][app]['title'] if app in self.conf['apps'] else self.appmgr.online_packages[app]['title']
|
self.queue.enqueue_action(app, action)
|
||||||
id,item = self.appmgr.enqueue_action(action, app)
|
response = self.render_json({'ok': self.render_setup_apps_table(request)})
|
||||||
response = self.render_json({'html': self.render_setup_apps_row(request, app, app_title, item), 'id': id})
|
response.call_on_close(self.queue.process_actions)
|
||||||
response.call_on_close(lambda: self.appmgr.process_action(id))
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def start_app_action(self, request):
|
def start_app_action(self, request):
|
||||||
# Queues application start along with its dependencies
|
# Queues application start along with its dependencies
|
||||||
return self.enqueue_action(request, 'start_app')
|
return self.enqueue_app_action(request, self.appmgr.start_app)
|
||||||
|
|
||||||
def stop_app_action(self, request):
|
def stop_app_action(self, request):
|
||||||
# Queues application stop along with its dependencies
|
# Queues application stop along with its dependencies
|
||||||
return self.enqueue_action(request, 'stop_app')
|
return self.enqueue_app_action(request, self.appmgr.stop_app)
|
||||||
|
|
||||||
def install_app_action(self, request):
|
def install_app_action(self, request):
|
||||||
# Queues application installation
|
# Queues application installation
|
||||||
return self.enqueue_action(request, 'install_app')
|
return self.enqueue_app_action(request, self.appmgr.install_app)
|
||||||
|
|
||||||
def uninstall_app_action(self, request):
|
def uninstall_app_action(self, request):
|
||||||
# Queues application uninstallation
|
# Queues application uninstallation
|
||||||
return self.enqueue_action(request, 'uninstall_app')
|
return self.enqueue_app_action(request, self.appmgr.uninstall_app)
|
||||||
|
|
||||||
def get_progress_action(self, request):
|
def get_app_status_action(self, request):
|
||||||
# Gets appmgr queue status for given ids
|
# Gets application and queue status
|
||||||
json = {}
|
return self.render_json({'ok': self.render_setup_apps_table(request)})
|
||||||
|
|
||||||
|
def clear_app_status_action(self, request):
|
||||||
|
# Clears error status for an application
|
||||||
try:
|
try:
|
||||||
ids = request.form.getlist('ids[]')
|
app = request.form['app']
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
return self.render_json({'error': request.session.lang.malformed_request()})
|
return self.render_json({'error': request.session.lang.malformed_request()})
|
||||||
actions = self.appmgr.get_actions(ids)
|
self.queue.clear_action(app)
|
||||||
for id,item in actions.items():
|
return self.render_json({'ok': self.render_setup_apps_table(request)})
|
||||||
app = item.app
|
|
||||||
# In case of installation error, we need to get the name from online_packages as the app is not yet registered
|
|
||||||
app_title = self.conf['apps'][app]['title'] if app in self.conf['apps'] else self.appmgr.online_packages[app]['title']
|
|
||||||
json[id] = {'html': self.render_setup_apps_row(request, app, app_title, item), 'last': item.finished}
|
|
||||||
return self.render_json(json)
|
|
||||||
|
|
||||||
def update_password_action(self, request):
|
def update_password_action(self, request):
|
||||||
# Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account
|
# Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account
|
||||||
|
@ -27,6 +27,20 @@ class WSGILang:
|
|||||||
'password_changed': 'Heslo úspěšně změněno',
|
'password_changed': 'Heslo úspěšně změněno',
|
||||||
'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.',
|
'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.',
|
||||||
'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.',
|
'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.',
|
||||||
|
'status_queued': 've frontě',
|
||||||
|
'status_starting': 'Spouští se',
|
||||||
|
'status_started': 'Spuštěna',
|
||||||
|
'status_stopping': 'Zastavuje se',
|
||||||
|
'status_stopped': 'Zastavena',
|
||||||
|
'status_downloading': 'Stahuje se',
|
||||||
|
'status_installing_deps': 'Instalují se závislosti',
|
||||||
|
'status_installing': 'Instaluje se',
|
||||||
|
'status_uninstalling': 'Odinstalovává se',
|
||||||
|
'status_not_installed': 'Není nainstalována',
|
||||||
|
'action_start': 'Spustit',
|
||||||
|
'action_stop': 'Zastavit',
|
||||||
|
'action_install': 'Instalovat',
|
||||||
|
'action_uninstall': 'Odinstalovat',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
var action_queue = [];
|
var status_interval;
|
||||||
|
var conn_fail_counter = 0;
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#update-host').on('submit', update_host);
|
$('#update-host').on('submit', update_host);
|
||||||
@ -14,11 +15,14 @@ $(function() {
|
|||||||
.on('click', '.app-start', start_app)
|
.on('click', '.app-start', start_app)
|
||||||
.on('click', '.app-stop', stop_app)
|
.on('click', '.app-stop', stop_app)
|
||||||
.on('click', '.app-install', install_app)
|
.on('click', '.app-install', install_app)
|
||||||
.on('click', '.app-uninstall', uninstall_app);
|
.on('click', '.app-uninstall', uninstall_app)
|
||||||
|
.on('click', '.app-clear-status', clear_app_status);
|
||||||
$('#update-password').on('submit', update_password);
|
$('#update-password').on('submit', update_password);
|
||||||
$('#reboot-vm').on('click', reboot_vm);
|
$('#reboot-vm').on('click', reboot_vm);
|
||||||
$('#shutdown-vm').on('click', shutdown_vm);
|
$('#shutdown-vm').on('click', shutdown_vm);
|
||||||
window.setInterval(check_progress, 1000);
|
if ($('#app-manager').length) {
|
||||||
|
status_interval = setInterval(get_app_status, 1000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function update_host() {
|
function update_host() {
|
||||||
@ -140,56 +144,60 @@ function update_app_autostart(ev) {
|
|||||||
return _update_app('autostart', ev);
|
return _update_app('autostart', ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _do_app(action, ev) {
|
function _do_app(url, ev) {
|
||||||
var el = $(ev.target);
|
var el = $(ev.target);
|
||||||
var tr = el.closest('tr');
|
var tr = el.closest('tr');
|
||||||
var td = el.closest('td');
|
var td = el.closest('td');
|
||||||
td.html('<div class="loader"></div>');
|
td.html('<div class="loader"></div>');
|
||||||
$.post('/'+action+'-app', {'app': tr.data('app')}, function(data) {
|
clearInterval(status_interval);
|
||||||
|
$.post(url, {'app': tr.data('app')}, function(data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
td.attr('class','error').html(data.error);
|
alert(data.error);
|
||||||
} else if (action) {
|
} else {
|
||||||
tr.html(data.html);
|
$('#app-manager tbody').html(data.ok);
|
||||||
action_queue.push(data.id);
|
|
||||||
}
|
}
|
||||||
|
status_interval = setInterval(get_app_status, 1000);
|
||||||
|
}).fail(function() {
|
||||||
|
alert('Spojení se serverem bylo ztraceno');
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function start_app(ev) {
|
function start_app(ev) {
|
||||||
return _do_app('start', ev);
|
return _do_app('/start-app', ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop_app(ev) {
|
function stop_app(ev) {
|
||||||
return _do_app('stop', ev);
|
return _do_app('/stop-app', ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_app(ev) {
|
function install_app(ev) {
|
||||||
return _do_app('install', ev);
|
return _do_app('/install-app', ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall_app(ev) {
|
function uninstall_app(ev) {
|
||||||
var app = $(ev.target).closest('tr').children().first().text()
|
var app = $(ev.target).closest('tr').children().first().text()
|
||||||
if (confirm('Opravdu chcete odinstalovat aplikaci '+app+'?')) {
|
if (confirm('Opravdu chcete odinstalovat aplikaci '+app+'?')) {
|
||||||
return _do_app('uninstall', ev);
|
return _do_app('/uninstall-app', ev);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_progress() {
|
function clear_app_status(ev) {
|
||||||
if (action_queue.length) {
|
return _do_app('/clear-app-status', ev);
|
||||||
$.post('/get-progress', {'ids': action_queue}, function(data) {
|
}
|
||||||
for (id in data) {
|
|
||||||
var app = id.split(':')[0];
|
function get_app_status() {
|
||||||
$('#app-manager tr[data-app="'+app+'"]').html(data[id].html);
|
$.get('/get-app-status', function(data) {
|
||||||
if (data[id].last) {
|
$('#app-manager tbody').html(data.ok);
|
||||||
action_queue = action_queue.filter(function(item) {
|
conn_fail_counter = 0;
|
||||||
return item !== id
|
}).fail(function() {
|
||||||
});
|
conn_fail_counter++;
|
||||||
}
|
if (conn_fail_counter == 10) {
|
||||||
}
|
alert('Spojení se serverem bylo ztraceno');
|
||||||
});
|
clearInterval(status_interval);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_password() {
|
function update_password() {
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="author" content="TS">
|
|
||||||
<meta name="copyright" content="page is under CC BY-NC-ND 3.0 CZ">
|
|
||||||
<meta name="generator" content="Spotter.ngo">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Chyba 404</title>
|
<title>Chyba 404</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -2,14 +2,11 @@
|
|||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="author" content="TS">
|
|
||||||
<meta name="copyright" content="page is under CC BY-NC-ND 3.0 CZ">
|
|
||||||
<meta name="generator" content="Spotter.ngo">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Chyba 502</title>
|
<title>Chyba 502</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Chyba spojení s aplikací</h1>
|
<h1>Chyba spojení s aplikací</h1>
|
||||||
<p>Aplikace ke které se pokoušíte připojit není dostupná. Nejspíše byla vypnuta správcem serveru.</p>
|
<p>Aplikace, ke které se pokoušíte připojit, není dostupná. Nejspíše se právě spouští nebo zastavuje. Počkejte chvíli a obnovte stránku.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
12
usr/share/vmmgr/templates/503.html
Normal file
12
usr/share/vmmgr/templates/503.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Chyba 503</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Aplikace není dostupná</h1>
|
||||||
|
<p>Aplikace, ke které se pokoušíte připojit, není dostupná. Nejspíše byla vypnuta správcem serveru.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,23 +0,0 @@
|
|||||||
{% set not_installed = app not in conf['apps'] %}
|
|
||||||
{% if not status %}
|
|
||||||
{% if not_installed: %}
|
|
||||||
{% set status = 'Není nainstalována' %}
|
|
||||||
{% set actions = '<a href="#" class="app-install">Instalovat</a>' %}
|
|
||||||
{% elif is_service_started(app): %}
|
|
||||||
{% set status = '<span class="info">Spuštěna</span>' %}
|
|
||||||
{% set actions = '<a href="#" class="app-stop">Zastavit</a>' %}
|
|
||||||
{% else: %}
|
|
||||||
{% set status = '<span class="error">Zastavena</span>' %}
|
|
||||||
{% set actions = '<a href="#" class="app-start">Spustit</a>, <a href="#" class="app-uninstall">Odinstalovat</a>' %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<td>{{ app_title }}</td>
|
|
||||||
<td class="center"><input type="checkbox" class="app-visible"{% if not_installed %} disabled{% elif conf['apps'][app]['visible'] %} checked{% endif %}></td>
|
|
||||||
<td class="center"><input type="checkbox" class="app-autostart"{% if not_installed %} disabled{% elif is_service_autostarted(app) %} checked{% endif %}></td>
|
|
||||||
{% if is_error %}
|
|
||||||
<td colspan="2">{{ status|safe }}</td>
|
|
||||||
{% else %}
|
|
||||||
<td>{{ status|safe }}</td>
|
|
||||||
<td>{{ actions|safe }}</td>
|
|
||||||
{% endif %}
|
|
13
usr/share/vmmgr/templates/setup-apps-table.html
Normal file
13
usr/share/vmmgr/templates/setup-apps-table.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% for app,data in app_data.items() %}
|
||||||
|
<tr data-app="{{ app }}">
|
||||||
|
<td>{{ data['title'] }}</td>
|
||||||
|
<td class="center"><input type="checkbox" class="app-visible"{% if not data['installed'] %} disabled{% elif data['visible'] %} checked{% endif %}></td>
|
||||||
|
<td class="center"><input type="checkbox" class="app-autostart"{% if not data['installed'] %} disabled{% elif data['autostarted'] %} checked{% endif %}></td>
|
||||||
|
{% if not data['actions'] %}
|
||||||
|
<td colspan="2">{{ data['status']|safe }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ data['status']|safe }}</td>
|
||||||
|
<td>{{ data['actions']|safe }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
@ -15,15 +15,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for app in all_apps %}
|
{{ table|safe }}
|
||||||
{% set app_title = conf['apps'][app]['title'] if app in conf['apps'] else online_packages[app]['title'] %}
|
|
||||||
<tr data-app="{{ app }}">
|
|
||||||
{% include 'setup-apps-row.html' %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if not online_packages %}
|
{% if not repo_reachable %}
|
||||||
<p class="error">Připojení k distribučnímu serveru se nezdařilo. Zkontrolujte přístupové údaje a připojení k síti.</p>
|
<p class="error">Připojení k distribučnímu serveru se nezdařilo. Zkontrolujte přístupové údaje a připojení k síti.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><strong>Přístupové údaje k distribučnímu serveru:</strong></p>
|
<p><strong>Přístupové údaje k distribučnímu serveru:</strong></p>
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<ul style="column-count:3">
|
<ul style="column-count:3">
|
||||||
<li>{{ conf['host']['domain'] }}</li>
|
<li>{{ conf['host']['domain'] }}</li>
|
||||||
{% for app in conf['apps']|sort %}
|
{% for app in conf['apps']|sort %}
|
||||||
<li>{{ conf['apps'][app]['host'] }}.{{ conf['host']['domain'] }}</li>
|
<li>{{ conf['packages'][app]['host'] }}.{{ conf['host']['domain'] }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<input type="button" id="verify-dns" value="Ověřit nastavení DNS">
|
<input type="button" id="verify-dns" value="Ověřit nastavení DNS">
|
||||||
|
Loading…
Reference in New Issue
Block a user