Implement global queue, improve failure resiliency

This commit is contained in:
Disassembler 2018-11-04 19:49:15 +01:00
parent ace50a79e7
commit 8f7cb14305
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
16 changed files with 424 additions and 338 deletions

View File

@ -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)

View File

@ -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)

View 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]

View File

@ -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:

View File

@ -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)

View File

@ -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]))

View File

@ -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

View File

@ -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):

View File

@ -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() {

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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 %}

View 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 %}

View File

@ -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>

View File

@ -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">