Initial refactor commit after SPOC
This commit is contained in:
parent
f5501c0605
commit
fb38e535e1
@ -1,14 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import Enum
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
from spoc.config import LOCK_FILE
|
||||
from spoc.flock import locked
|
||||
|
||||
class ActionItemType(Enum):
|
||||
IMAGE_DOWNLOAD = 1
|
||||
IMAGE_UNPACK = 2
|
||||
IMAGE_DELETE = 3
|
||||
APP_DOWNLOAD = 4
|
||||
APP_UNPACK = 5
|
||||
APP_INSTALL = 6
|
||||
APP_UPDATE = 7
|
||||
APP_UNINSTALL = 8
|
||||
|
||||
class ActionItem:
|
||||
def __init__(self, key, action):
|
||||
def __init__(self, type, key, action, show_progress=True):
|
||||
self.type = type
|
||||
self.key = key
|
||||
self.action = action
|
||||
self.show_progress = show_progress
|
||||
self.units_total = 1
|
||||
self.units_done = 0
|
||||
|
||||
def run(self):
|
||||
if self.show_progress:
|
||||
self.action(self)
|
||||
else:
|
||||
self.action()
|
||||
self.units_done = 1
|
||||
|
||||
class ActionAppQueue:
|
||||
def __init__(self, action):
|
||||
self.action = action
|
||||
self.queue = []
|
||||
self.started = False
|
||||
self.data = None
|
||||
self.exception = None
|
||||
self.index = 0
|
||||
|
||||
def download_image(self, image):
|
||||
self.queue.append(ActionItem(ActionItemType.IMAGE_DOWNLOAD, image.name, image.download))
|
||||
self.queue.append(ActionItem(ActionItemType.IMAGE_UNPACK, image.name, image.unpack_downloaded))
|
||||
|
||||
def delete_image(self, image):
|
||||
self.queue.append(ActionItem(ActionItemType.IMAGE_DELETE, image.name, image.delete, False))
|
||||
|
||||
def install_app(self, app):
|
||||
self.queue.append(ActionItem(ActionItemType.APP_DOWNLOAD, app.name, app.download))
|
||||
self.queue.append(ActionItem(ActionItemType.APP_UNPACK, app.name, app.unpack_downloaded))
|
||||
self.queue.append(ActionItem(ActionItemType.APP_INSTALL, app.name, app.install, False))
|
||||
|
||||
def update_app(self, app):
|
||||
self.queue.append(ActionItem(ActionItemType.APP_DOWNLOAD, app.name, app.download))
|
||||
self.queue.append(ActionItem(ActionItemType.APP_UNPACK, app.name, app.unpack_downloaded))
|
||||
self.queue.append(ActionItem(ActionItemType.APP_UPDATE, app.name, app.update, False))
|
||||
|
||||
def uninstall_app(self, app):
|
||||
self.queue.append(ActionItem(ActionItemType.APP_UNINSTALL, app.name, app.uninstall, False))
|
||||
|
||||
def process(self):
|
||||
for item in self.queue:
|
||||
self.index += 1
|
||||
item.run()
|
||||
|
||||
class ActionQueue:
|
||||
def __init__(self):
|
||||
@ -18,20 +73,21 @@ class ActionQueue:
|
||||
self.is_running = False
|
||||
|
||||
def get_actions(self):
|
||||
# Return copy of actions, so they can be traversed without state changes
|
||||
# Return copy of actions, so they can be read and traversed without state changes
|
||||
with self.lock:
|
||||
return self.actions.copy()
|
||||
|
||||
def enqueue_action(self, key, action):
|
||||
def enqueue_action(self, app_name, action):
|
||||
# Enqueue action
|
||||
with self.lock:
|
||||
if key in self.actions:
|
||||
# If the key alredy has a pending action, reject any other actions
|
||||
if app_name in self.actions:
|
||||
# If the app already has a pending action, reject any other actions
|
||||
return
|
||||
item = ActionItem(key, action)
|
||||
self.actions[key] = item
|
||||
self.queue.append(item)
|
||||
# Create empty queue to be populated with actions just before execution
|
||||
self.actions[app_name] = ActionAppQueue(action)
|
||||
self.queue.append(app_name)
|
||||
|
||||
@locked(LOCK_FILE)
|
||||
def process_actions(self):
|
||||
# Main method for deferred queue processing called by WSGI close handler
|
||||
with self.lock:
|
||||
@ -41,28 +97,32 @@ class ActionQueue:
|
||||
while True:
|
||||
with self.lock:
|
||||
# Try to get an item from queue
|
||||
item = None
|
||||
app_name = None
|
||||
if self.queue:
|
||||
item = self.queue.popleft()
|
||||
app_name = self.queue.popleft()
|
||||
# If there are no more queued items, unset the processing flag and allow the thread to be terminated
|
||||
if not item:
|
||||
if not app_name:
|
||||
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
|
||||
app_queue = self.actions[app_name]
|
||||
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)
|
||||
# Call the method passed in app_queue.action to populate the queue of the actual actions to be taken in relation to the current local repository state
|
||||
app_queue.action(app_name, app_queue)
|
||||
# Process the freshly populated queue of actions related to the particular app
|
||||
app_queue.process()
|
||||
# If the actions finished without errors, restore nominal state by deleting the item from action list
|
||||
self.clear_action(app_name)
|
||||
except BaseException as e:
|
||||
# If the action failed, store the exception and leave it in the list for manual clearance
|
||||
with self.lock:
|
||||
item.data = e
|
||||
app_queue.exception = e
|
||||
|
||||
def clear_action(self, key):
|
||||
def clear_action(self, app_name):
|
||||
# Restore nominal state by deleting the item from action list
|
||||
with self.lock:
|
||||
if key in self.actions:
|
||||
del self.actions[key]
|
||||
try:
|
||||
del self.actions[app_name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
@ -1,100 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
#from .pkgmgr import Pkg, PkgMgr
|
||||
|
||||
class AppMgr:
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.pkgmgr = None #PkgMgr(conf)
|
||||
|
||||
def start_app(self, item):
|
||||
# Start the actual app service
|
||||
app = item.key
|
||||
if app in self.conf['apps'] and not self.is_service_started(app):
|
||||
self.start_service(app)
|
||||
|
||||
def start_service(self, service):
|
||||
subprocess.run(['/sbin/service', service, 'start'], check=True)
|
||||
|
||||
def stop_app(self, item):
|
||||
# Stop the actual app service
|
||||
app = item.key
|
||||
if app in self.conf['apps'] and self.is_service_started(app):
|
||||
self.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 self.get_service_deps(app):
|
||||
if not any([self.is_service_started(d) for d in deps[dep]]):
|
||||
self.stop_service(dep)
|
||||
|
||||
def stop_service(self, service):
|
||||
subprocess.run(['/sbin/service', service, 'stop'], check=True)
|
||||
|
||||
def update_app_visibility(self, app, visible):
|
||||
# Update visibility for the app in the configuration
|
||||
if app in self.conf['apps']:
|
||||
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 in self.conf['apps']:
|
||||
subprocess.run(['/sbin/rc-update', 'add' if enabled else 'del', app])
|
||||
|
||||
def is_service_started(self, app):
|
||||
# Check OpenRC service status without calling any binary
|
||||
return os.path.exists(os.path.join('/run/openrc/started', app))
|
||||
|
||||
def is_service_autostarted(self, app):
|
||||
# Check OpenRC service enablement
|
||||
return os.path.exists(os.path.join('/etc/runlevels/default', app))
|
||||
|
||||
def install_app(self, item):
|
||||
# Main installation function. Wrapper for download, registration and install script
|
||||
item.data = None #Pkg()
|
||||
self.pkgmgr.install_app(item.key, item.data)
|
||||
|
||||
def uninstall_app(self, item):
|
||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
||||
app = item.key
|
||||
self.stop_app(item)
|
||||
if self.is_service_autostarted(app):
|
||||
self.update_app_autostart(app, False)
|
||||
self.pkgmgr.uninstall_app(app)
|
||||
|
||||
def update_app(self, item):
|
||||
# Main update function. Wrapper for download and update script
|
||||
self.stop_app(item)
|
||||
item.data = None #Pkg()
|
||||
self.pkgmgr.update_app(item.key, item.data)
|
||||
|
||||
def get_services_deps(self):
|
||||
# Fisrt, build a dictionary of {app: [needs]}
|
||||
needs = {}
|
||||
for app in self.conf['apps'].copy():
|
||||
needs[app] = self.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 get_service_deps(self, app):
|
||||
# Get "need" line from init script and split it to a list
|
||||
try:
|
||||
with open(os.path.join('/etc/init.d', app), 'r') as f:
|
||||
return [l for l in f.readlines() if l.strip().startswith('need')][0].split()[1:]
|
||||
except:
|
||||
pass
|
||||
return []
|
||||
|
||||
def update_repo_settings(self, url, user, pwd):
|
||||
# Update lxc repository configuration
|
||||
self.conf['repo']['url'] = url
|
||||
self.conf['repo']['user'] = user
|
||||
self.conf['repo']['pwd'] = pwd
|
||||
self.conf.save()
|
@ -1,27 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from spoc.flock import locked
|
||||
|
||||
from .paths import CONF_FILE, CONF_LOCK
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.load()
|
||||
data = {}
|
||||
mtime = None
|
||||
|
||||
def load(self):
|
||||
# Load configuration from file. Uses file lock as interprocess mutex
|
||||
with open(CONF_LOCK, 'w') as lock:
|
||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||
with open(CONF_FILE, 'r') as f:
|
||||
self.data = json.load(f)
|
||||
def load():
|
||||
global data
|
||||
global mtime
|
||||
file_mtime = os.stat(CONF_FILE).st_mtime
|
||||
if mtime != file_mtime:
|
||||
with open(CONF_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
mtime = file_mtime
|
||||
|
||||
def save(self):
|
||||
# Save configuration to a file. Uses file lock as interprocess mutex
|
||||
with open(CONF_LOCK, 'w') as lock:
|
||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||
with open(CONF_FILE, 'w') as f:
|
||||
json.dump(self.data, f, sort_keys=True, indent=4)
|
||||
def save():
|
||||
global mtime
|
||||
with open(CONF_FILE, 'w') as f:
|
||||
json.dump(data, f, sort_keys=True, indent=4)
|
||||
mtime = os.stat(CONF_FILE).st_mtime
|
||||
|
||||
def __getitem__(self, attr):
|
||||
return self.data[attr]
|
||||
@locked(CONF_LOCK)
|
||||
def get_entries(attr):
|
||||
load()
|
||||
return data[attr]
|
||||
|
||||
@locked(CONF_LOCK)
|
||||
def add_entry(entry_type, name, definition):
|
||||
load()
|
||||
data[entry_type][name] = definition
|
||||
save()
|
||||
|
||||
@locked(CONF_LOCK)
|
||||
def delete_entry(entry_type, name):
|
||||
load()
|
||||
try:
|
||||
del data[entry_type][name]
|
||||
save()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_apps():
|
||||
return get_entries('apps')
|
||||
|
||||
def get_common():
|
||||
return get_entries('common')
|
||||
|
||||
def get_host():
|
||||
host = get_entries('host')
|
||||
return (host['domain'], host['port'])
|
||||
|
||||
def get_adminpwd():
|
||||
return get_entries('host')['adminpwd']
|
||||
|
||||
def register_app(app_name, definition):
|
||||
add_entry('apps', app_name, definition)
|
||||
|
||||
def unregister_app(app_name):
|
||||
delete_entry('apps', app_name)
|
||||
|
||||
def set_common(key, value):
|
||||
add_entry('common', key, value)
|
||||
|
||||
@locked(CONF_LOCK)
|
||||
def set_host(domain, port):
|
||||
load()
|
||||
data['host']['domain'] = domain
|
||||
data['host']['port'] = port
|
||||
save()
|
||||
|
||||
def set_adminpwd(hash):
|
||||
add_entry('host', 'adminpwd', hash)
|
||||
|
||||
@locked(CONF_LOCK)
|
||||
def set_app(app_name, key, value):
|
||||
load()
|
||||
data['apps'][app_name][key] = value
|
||||
save()
|
||||
|
@ -3,17 +3,18 @@
|
||||
import bcrypt
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
|
||||
|
||||
from . import config
|
||||
from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE
|
||||
|
||||
def create_selfsigned_cert(domain):
|
||||
def create_selfsigned_cert():
|
||||
# Create selfsigned certificate with wildcard alternative subject name
|
||||
domain = config.get_host()[0]
|
||||
private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())
|
||||
public_key = private_key.public_key()
|
||||
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, domain)])
|
||||
@ -25,7 +26,7 @@ def create_selfsigned_cert(domain):
|
||||
.serial_number(x509.random_serial_number()) \
|
||||
.not_valid_before(now) \
|
||||
.not_valid_after(now + datetime.timedelta(days=7305)) \
|
||||
.add_extension(x509.SubjectAlternativeName((x509.DNSName(domain), x509.DNSName('*.{}'.format(domain)))), critical=False) \
|
||||
.add_extension(x509.SubjectAlternativeName((x509.DNSName(domain), x509.DNSName(f'*.{domain}'))), critical=False) \
|
||||
.add_extension(x509.SubjectKeyIdentifier.from_public_key(public_key), critical=False) \
|
||||
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key), critical=False) \
|
||||
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) \
|
||||
@ -44,7 +45,7 @@ def get_cert_info():
|
||||
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
|
||||
data = {'subject': cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
|
||||
'issuer': cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
|
||||
'expires': '{} UTC'.format(cert.not_valid_after),
|
||||
'expires': f'{cert.not_valid_after} UTC',
|
||||
'method': 'manual'}
|
||||
if os.access(ACME_CRON, os.X_OK):
|
||||
data['method'] = 'automatic'
|
||||
@ -58,5 +59,5 @@ def get_cert_info():
|
||||
def adminpwd_hash(password):
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
def adminpwd_verify(password, pwhash):
|
||||
return bcrypt.checkpw(password.encode(), pwhash.encode())
|
||||
def adminpwd_verify(password):
|
||||
return bcrypt.checkpw(password.encode(), config.get_adminpwd().encode())
|
||||
|
@ -9,8 +9,8 @@ import subprocess
|
||||
from .paths import MYIP_URL, PING_URL
|
||||
|
||||
def compile_url(domain, port, proto='https'):
|
||||
port = '' if (proto == 'https' and port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port)
|
||||
return '{}://{}{}'.format(proto, domain, port)
|
||||
port = '' if (proto in (None, 'https') and port == '443') or (proto == 'http' and port == '80') else f':{port}'
|
||||
return f'{proto}://{domain}{port}' if proto else f'{domain}{port}'
|
||||
|
||||
def get_local_ip(version=None):
|
||||
# Return first routable IPv4/6 address of the VM (container host)
|
||||
|
@ -24,4 +24,3 @@ WG_CONF_FILE_DISABLED = '/etc/wireguard/wg0.conf.disabled'
|
||||
# URLs
|
||||
MYIP_URL = 'https://repo.spotter.cz/tools/myip.php'
|
||||
PING_URL = 'https://repo.spotter.cz/tools/vm-ping.php'
|
||||
RELOAD_URL = 'http://127.0.0.1:8080/reload-config'
|
||||
|
@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from . import templates
|
||||
from .paths import AUTHORIZED_KEYS, INTERFACES_FILE, WG_CONF_FILE, WG_CONF_FILE_DISABLED
|
||||
|
||||
def get_authorized_keys():
|
||||
@ -38,7 +36,7 @@ def regenerate_wireguard_key():
|
||||
privkey = subprocess.run(['wg', 'genkey'], stdout=subprocess.PIPE).stdout.strip().decode()
|
||||
with open(WG_CONF_FILE_DISABLED) as f:
|
||||
conf_lines = f.readlines()
|
||||
conf_lines[2] = 'PrivateKey = {}\n'.format(privkey)
|
||||
conf_lines[2] = f'PrivateKey = {privkey}\n'
|
||||
with open(WG_CONF_FILE_DISABLED, 'w') as f:
|
||||
f.writelines(conf_lines)
|
||||
if was_running:
|
||||
@ -77,14 +75,14 @@ def set_wireguard_conf(ip, port, peers):
|
||||
with open(INTERFACES_FILE) as f:
|
||||
for line in f.readlines():
|
||||
if '172.17.255' in line:
|
||||
line = ' address 172.17.255.{}\n'.format(ip)
|
||||
line = f' address 172.17.255.{ip}\n'
|
||||
interface_lines.append(line)
|
||||
with open(INTERFACES_FILE, 'w') as f:
|
||||
f.writelines(interface_lines)
|
||||
# Recreate config (listen port and peers)
|
||||
with open(WG_CONF_FILE_DISABLED) as f:
|
||||
conf_lines = f.readlines()[:4]
|
||||
conf_lines[1] = 'ListenPort = {}\n'.format(port)
|
||||
conf_lines[1] = f'ListenPort = {port}\n'
|
||||
with open(WG_CONF_FILE_DISABLED, 'w') as f:
|
||||
f.writelines(conf_lines)
|
||||
f.write(peers)
|
||||
|
@ -1,149 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import configparser
|
||||
import os
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib
|
||||
from spoc.app import App
|
||||
from spoc.config import ONLINE_BASE_URL
|
||||
from spoc.container import Container, ContainerState
|
||||
|
||||
from . import crypto
|
||||
from . import templates
|
||||
from . import net
|
||||
from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR, RELOAD_URL
|
||||
from . import crypto, net, templates
|
||||
from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR
|
||||
|
||||
class VMMgr:
|
||||
def __init__(self, conf):
|
||||
# Load JSON configuration
|
||||
self.conf = conf
|
||||
self.domain = conf['host']['domain']
|
||||
self.port = conf['host']['port']
|
||||
def register_app(app, host, login, password):
|
||||
# Register newly installed application, its subdomain and credentials (called at the end of package install.sh)
|
||||
config.register_app(app, {
|
||||
'host': host,
|
||||
'login': login if login else 'N/A',
|
||||
'password': password if password else 'N/A',
|
||||
'visible': False,
|
||||
})
|
||||
|
||||
def register_app(self, app, host, login, password):
|
||||
# Register newly installed application, its subdomain and credentials (called at the end of package install.sh)
|
||||
self.conf['apps'][app] = {'host': host,
|
||||
'login': login if login else 'N/A',
|
||||
'password': password if password else 'N/A',
|
||||
'visible': False}
|
||||
self.conf.save()
|
||||
self.reload_wsgi_config()
|
||||
def unregister_app(app):
|
||||
config.unregister_app(app)
|
||||
|
||||
def unregister_app(self, app):
|
||||
# Unregister application during uninstallation (called at the end of package uninstall.sh)
|
||||
if app not in self.conf['apps']:
|
||||
return
|
||||
del self.conf['apps'][app]
|
||||
self.conf.save()
|
||||
self.reload_wsgi_config()
|
||||
def register_proxy(app):
|
||||
# Setup proxy configuration and reload nginx
|
||||
app_host = config.get_app(app)['host']
|
||||
domain,port = config.get_host()
|
||||
with open(os.path.join(NGINX_DIR, f'{app}.conf'), 'w') as f:
|
||||
f.write(templates.NGINX.format(app=app, host=app_host, domain=domain, port=port))
|
||||
reload_nginx()
|
||||
|
||||
def reload_wsgi_config(self):
|
||||
# Attempt to contact running vmmgr WSGI application to reload config
|
||||
def unregister_proxy(app):
|
||||
# Remove proxy configuration and reload nginx
|
||||
try:
|
||||
os.unlink(os.path.join(NGINX_DIR, f'{app}.conf'))
|
||||
reload_nginx()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def update_host(domain, port):
|
||||
config.set_host(domain, port)
|
||||
# Rebuild nginx config for the portal app. Web interface calls restart_nginx() in WSGI close handler
|
||||
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
|
||||
f.write(templates.NGINX_DEFAULT.format(port=port, domain_esc=domain.replace('.', '\\.')))
|
||||
|
||||
def reload_nginx():
|
||||
subprocess.run(['/usr/sbin/nginx', '-s', 'reload'])
|
||||
|
||||
def restart_nginx():
|
||||
subprocess.run(['/sbin/service', 'nginx', 'restart'])
|
||||
|
||||
def rebuild_issue():
|
||||
# Compile the URLs displayed in terminal banner and rebuild the issue and motd files
|
||||
domain, port = config.get_host()
|
||||
issue = templates.ISSUE.format(url=net.compile_url(domain, port), ip=net.compile_url(net.get_local_ip(), port))
|
||||
with open(ISSUE_FILE, 'w') as f:
|
||||
f.write(issue)
|
||||
with open(MOTD_FILE, 'w') as f:
|
||||
f.write(issue)
|
||||
|
||||
def update_common_settings(email, gmaps_api_key):
|
||||
# Update common configuration values
|
||||
config.set_common('email', email)
|
||||
config.set_common('gmaps-api-key', gmaps_api_key)
|
||||
|
||||
def update_password(oldpassword, newpassword):
|
||||
# Update LUKS password and adminpwd for WSGI application
|
||||
pwinput = f'{oldpassword}\n{newpassword}'.encode()
|
||||
partition_uuid = open('/etc/crypttab').read().split()[1][5:]
|
||||
partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
|
||||
subprocess.run(['cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True)
|
||||
# Update bcrypt-hashed password in config
|
||||
config.set_adminpwd(crypto.adminpwd_hash(newpassword))
|
||||
|
||||
def create_selfsigned_cert():
|
||||
# Disable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o640)
|
||||
# Create selfsigned certificate with wildcard alternative subject name
|
||||
domain = config.get_host()[0]
|
||||
crypto.create_selfsigned_cert(domain)
|
||||
# Reload nginx
|
||||
reload_nginx()
|
||||
|
||||
def request_acme_cert():
|
||||
# Remove all possible conflicting certificates requested in the past
|
||||
domain = config.get_host()[0]
|
||||
certs = [i for i in os.listdir(ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')]
|
||||
for cert in certs:
|
||||
if cert != domain:
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--remove', '-d', cert])
|
||||
# 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(ACME_DIR, domain)):
|
||||
cmd = ['/usr/bin/acme.sh', '--issue', '-d', domain]
|
||||
for app,definition in config.get_apps():
|
||||
cmd += ['-d', f'{definition["host"]}.{domain}']
|
||||
cmd += ['-w', ACME_DIR]
|
||||
# Request the certificate
|
||||
subprocess.run(cmd, check=True)
|
||||
# Otherwise just try to renew
|
||||
else:
|
||||
# Acme.sh returns code 2 on skipped renew
|
||||
try:
|
||||
requests.get(RELOAD_URL, timeout=3)
|
||||
except:
|
||||
pass
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--renew', '-d', domain], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 2:
|
||||
raise
|
||||
# Install the issued certificate
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--install-cert', '-d', domain, '--key-file', crypto.CERT_KEY_FILE, '--fullchain-file', crypto.CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
|
||||
# Enable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o750)
|
||||
|
||||
def register_proxy(self, app):
|
||||
# Setup proxy configuration and reload nginx
|
||||
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
||||
f.write(templates.NGINX.format(app=app, host=self.conf['apps'][app]['host'], domain=self.conf['host']['domain'], port=self.conf['host']['port']))
|
||||
self.reload_nginx()
|
||||
def install_manual_cert(public_file, private_file):
|
||||
# Disable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o640)
|
||||
# Copy certificate files
|
||||
shutil.copyfile(public_file, crypto.CERT_PUB_FILE)
|
||||
shutil.copyfile(private_file, crypto.CERT_KEY_FILE)
|
||||
os.chmod(crypto.CERT_KEY_FILE, 0o600)
|
||||
# Reload nginx
|
||||
reload_nginx()
|
||||
|
||||
def unregister_proxy(self, app):
|
||||
# Remove proxy configuration and reload nginx
|
||||
try:
|
||||
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
||||
self.reload_nginx()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
def shutdown_vm():
|
||||
subprocess.run(['/sbin/poweroff'])
|
||||
|
||||
def update_host(self, domain, port):
|
||||
# Update domain and port, rebuild all configuration and restart nginx
|
||||
self.domain = self.conf['host']['domain'] = domain
|
||||
self.port = self.conf['host']['port'] = port
|
||||
self.conf.save()
|
||||
# Rebuild nginx config for the portal app. Web interface calls restart_nginx() in WSGI close handler
|
||||
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
|
||||
f.write(templates.NGINX_DEFAULT.format(port=self.port, domain_esc=self.domain.replace('.', '\\.')))
|
||||
def reboot_vm():
|
||||
subprocess.run(['/sbin/reboot'])
|
||||
|
||||
def reload_nginx(self):
|
||||
subprocess.run(['/usr/sbin/nginx', '-s', 'reload'])
|
||||
def start_app(item):
|
||||
# Start the actual app service
|
||||
app = item.key
|
||||
if app in config.get_apps() and not is_app_started(app):
|
||||
start_service(app)
|
||||
|
||||
def restart_nginx(self):
|
||||
subprocess.run(['/sbin/service', 'nginx', 'restart'])
|
||||
def start_service(service):
|
||||
subprocess.run(['/sbin/service', service, 'start'], check=True)
|
||||
|
||||
def rebuild_issue(self):
|
||||
# Compile the URLs displayed in terminal banner and rebuild the issue and motd files
|
||||
issue = templates.ISSUE.format(url=net.compile_url(self.domain, self.port), ip=net.compile_url(net.get_local_ip(), self.port))
|
||||
with open(ISSUE_FILE, 'w') as f:
|
||||
f.write(issue)
|
||||
with open(MOTD_FILE, 'w') as f:
|
||||
f.write(issue)
|
||||
def stop_app(item):
|
||||
# Stop the actual app service
|
||||
app = item.key
|
||||
if app in config.get_apps() and is_app_started(app):
|
||||
stop_service(app)
|
||||
# Stop the app service's dependencies if they are not used by another running app
|
||||
deps = get_services_deps()
|
||||
for dep in get_service_deps(app):
|
||||
if not any([is_app_started(d) for d in deps[dep]]):
|
||||
stop_service(dep)
|
||||
|
||||
def update_common_settings(self, email, gmaps_api_key):
|
||||
# Update common configuration values
|
||||
self.conf['common']['email'] = email
|
||||
self.conf['common']['gmaps-api-key'] = gmaps_api_key
|
||||
self.conf.save()
|
||||
def stop_service(service):
|
||||
subprocess.run(['/sbin/service', service, 'stop'], check=True)
|
||||
|
||||
def update_password(self, oldpassword, newpassword):
|
||||
# Update LUKS password and adminpwd for WSGI application
|
||||
pwinput = '{}\n{}'.format(oldpassword, newpassword).encode()
|
||||
partition_uuid = open('/etc/crypttab').read().split()[1][5:]
|
||||
partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
|
||||
subprocess.run(['cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True)
|
||||
# Update bcrypt-hashed password in config
|
||||
self.conf['host']['adminpwd'] = crypto.adminpwd_hash(newpassword)
|
||||
# Save config to file
|
||||
self.conf.save()
|
||||
def update_app_visibility(app_name, visible):
|
||||
# Update visibility for the app in the configuration
|
||||
config.set_app(app_name, 'visible', visible)
|
||||
|
||||
def create_selfsigned_cert(self):
|
||||
# Disable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o640)
|
||||
# Create selfsigned certificate with wildcard alternative subject name
|
||||
crypto.create_selfsigned_cert(self.domain)
|
||||
# Reload nginx
|
||||
self.reload_nginx()
|
||||
def update_app_autostart(app_name, enabled):
|
||||
# Add/remove the app to OpenRC default runlevel
|
||||
App(app_name).set_autostart(enabled)
|
||||
|
||||
def request_acme_cert(self):
|
||||
# Remove all possible conflicting certificates requested in the past
|
||||
certs = [i for i in os.listdir(ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')]
|
||||
for cert in certs:
|
||||
if cert != self.domain:
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--remove', '-d', cert])
|
||||
# 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(ACME_DIR, self.domain)):
|
||||
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
|
||||
for app in self.conf['apps'].copy():
|
||||
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
|
||||
cmd += ['-w', ACME_DIR]
|
||||
# Request the certificate
|
||||
subprocess.run(cmd, check=True)
|
||||
# Otherwise just try to renew
|
||||
else:
|
||||
# Acme.sh returns code 2 on skipped renew
|
||||
try:
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--renew', '-d', self.domain], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 2:
|
||||
raise
|
||||
# Install the issued certificate
|
||||
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--install-cert', '-d', self.domain, '--key-file', crypto.CERT_KEY_FILE, '--fullchain-file', crypto.CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
|
||||
# Enable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o750)
|
||||
def is_app_started(app_name):
|
||||
# Assume that the main container has always the same name as app
|
||||
return Container(app_name).get_status() == ContainerState.RUNNING
|
||||
|
||||
def install_manual_cert(self, public_file, private_file):
|
||||
# Disable acme.sh cronjob
|
||||
os.chmod(ACME_CRON, 0o640)
|
||||
# Copy certificate files
|
||||
shutil.copyfile(public_file, crypto.CERT_PUB_FILE)
|
||||
shutil.copyfile(private_file, crypto.CERT_KEY_FILE)
|
||||
os.chmod(crypto.CERT_KEY_FILE, 0o600)
|
||||
# Reload nginx
|
||||
self.reload_nginx()
|
||||
def is_app_autostarted(app_name):
|
||||
# Check OpenRC service enablement
|
||||
return App(app_name, False).autostart
|
||||
|
||||
def shutdown_vm(self):
|
||||
subprocess.run(['/sbin/poweroff'])
|
||||
def install_app(app_name, queue):
|
||||
# Main installation function. Wrapper for download, registration and install script
|
||||
required_images = []
|
||||
for container in repo_online.get_app(app_name)['containers'].values():
|
||||
required_images.extend(repo_online.get_image(container['image'])['layers'])
|
||||
local_images = repo_local.get_images()
|
||||
for layer in set(required_images):
|
||||
if layer not in local_images:
|
||||
queue.download_image(Image(layer, False))
|
||||
queue.install_app(App(app_name, False, False))
|
||||
|
||||
def reboot_vm(self):
|
||||
subprocess.run(['/sbin/reboot'])
|
||||
def uninstall_app(app_name, queue):
|
||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
||||
queue.uninstall_app(App(app_name, False))
|
||||
|
||||
def update_app(app_name, queue):
|
||||
# Main update function. Wrapper for download and update script
|
||||
required_images = []
|
||||
for container in repo_online.get_app(app_name)['containers'].values():
|
||||
required_images.extend(repo_online.get_image(container['image'])['layers'])
|
||||
local_images = repo_local.get_images()
|
||||
for layer in set(required_images):
|
||||
if layer not in local_images:
|
||||
queue.download_image(Image(layer, False))
|
||||
queue.update_app(App(app_name, False))
|
||||
|
||||
def update_repo_settings(url, username, password):
|
||||
# Include credentials in the repo URL and save to SPOC config
|
||||
spoc_config = configparser.ConfigParser()
|
||||
spoc_config.read('/etc/spoc/spoc.conf')
|
||||
parts = urllib.parse.urlsplit(url)
|
||||
netloc = f'{username}:{password}@{url}' if username or password else url
|
||||
url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
|
||||
spoc_config['repo']['url'] = ONLINE_BASE_URL = url
|
||||
with open('/etc/spoc/spoc.conf', 'w') as f:
|
||||
config.write(f)
|
||||
|
||||
def get_repo_settings():
|
||||
# Parse the SPOC config repo URL and return as tuple
|
||||
parts = urllib.parse.urlsplit(ONLINE_BASE_URL)
|
||||
netloc = parts.netloc.split('@', 1)[1] if parts.username or parts.password else parts.netloc
|
||||
url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
|
||||
return (url, parts.username, parts.password)
|
||||
|
@ -2,24 +2,18 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from math import floor
|
||||
from pkg_resources import parse_version
|
||||
from spoc import repo_online, repo_local
|
||||
from werkzeug.exceptions import HTTPException, NotFound, Unauthorized
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.utils import redirect
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
from . import crypto
|
||||
from . import net
|
||||
from . import remote
|
||||
from . import validator
|
||||
from .actionqueue import ActionQueue
|
||||
from .appmgr import AppMgr
|
||||
from .config import Config
|
||||
#from .pkgmgr import Stage
|
||||
from .vmmgr import VMMgr
|
||||
from . import config, crypto, net, remote, validator, vmmgr
|
||||
from .actionqueue import ActionQueue, ActionItemType
|
||||
from .wsgilang import WSGILang
|
||||
from .wsgisession import WSGISession
|
||||
|
||||
@ -27,12 +21,8 @@ SESSION_KEY = os.urandom(26)
|
||||
|
||||
class WSGIApp:
|
||||
def __init__(self):
|
||||
self.conf = Config()
|
||||
self.vmmgr = VMMgr(self.conf)
|
||||
self.appmgr = AppMgr(self.conf)
|
||||
self.queue = ActionQueue()
|
||||
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.url_map = Map((
|
||||
Rule('/', endpoint='portal_view'),
|
||||
Rule('/login', methods=['GET'], endpoint='login_view'),
|
||||
@ -72,9 +62,6 @@ class WSGIApp:
|
||||
Rule('/start-vpn', endpoint='start_vpn_action'),
|
||||
Rule('/stop-vpn', endpoint='stop_vpn_action'),
|
||||
))
|
||||
self.localhost_url_map = Map((
|
||||
Rule('/reload-config', endpoint='reload_config_action'),
|
||||
))
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
@ -89,12 +76,7 @@ class WSGIApp:
|
||||
return response(environ, start_response)
|
||||
|
||||
def dispatch_request(self, request):
|
||||
if request.session['admin']:
|
||||
url_map = self.admin_url_map
|
||||
elif request.remote_addr in ('127.0.0.1', '::1'):
|
||||
url_map = self.localhost_url_map
|
||||
else:
|
||||
url_map = self.url_map
|
||||
url_map = self.admin_url_map if request.session['admin'] else self.url_map
|
||||
adapter = url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
@ -113,7 +95,7 @@ class WSGIApp:
|
||||
|
||||
def render_template(self, template_name, request, **context):
|
||||
# Enhance context
|
||||
context['conf'] = self.conf
|
||||
context['config'] = config
|
||||
context['session'] = request.session
|
||||
context['lang'] = request.session.lang
|
||||
# Render template
|
||||
@ -144,11 +126,11 @@ class WSGIApp:
|
||||
def login_action(self, request):
|
||||
password = request.form['password']
|
||||
redir = request.form['redir']
|
||||
if crypto.adminpwd_verify(password, self.conf['host']['adminpwd']):
|
||||
if crypto.adminpwd_verify(password):
|
||||
request.session['admin'] = True
|
||||
return redirect('/{}'.format(redir))
|
||||
request.session['msg'] = 'login:error:{}'.format(request.session.lang.bad_password())
|
||||
return redirect('/login?redir={}'.format(redir)) if redir else redirect('/login')
|
||||
return redirect(f'/{redir}')
|
||||
request.session['msg'] = f'login:error:{request.session.lang.bad_password()}'
|
||||
return redirect(f'/login?redir={redir}') if redir else redirect('/login')
|
||||
|
||||
def logout_action(self, request):
|
||||
request.session.reset()
|
||||
@ -156,10 +138,12 @@ class WSGIApp:
|
||||
|
||||
def portal_view(self, request):
|
||||
# Default portal view.
|
||||
host = net.compile_url(self.conf['host']['domain'], self.conf['host']['port'])[8:]
|
||||
host = net.compile_url(*config.get_host(), None)
|
||||
apps = config.get_apps()
|
||||
visible_apps = [app for app,definition in apps.items() if definition['visible'] and vmmgr.is_app_started(app)]
|
||||
if request.session['admin']:
|
||||
return self.render_html('portal-admin.html', request, host=host)
|
||||
return self.render_html('portal-user.html', request, host=host)
|
||||
return self.render_html('portal-admin.html', request, host=host, apps=apps, visible_apps=visible_apps)
|
||||
return self.render_html('portal-user.html', request, host=host, apps=apps, visible_apps=visible_apps)
|
||||
|
||||
def setup_host_view(self, request):
|
||||
# Host setup view.
|
||||
@ -168,13 +152,17 @@ class WSGIApp:
|
||||
in_ipv4 = net.get_local_ip(4)
|
||||
in_ipv6 = net.get_local_ip(6)
|
||||
cert_info = crypto.get_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)
|
||||
apps = config.get_apps()
|
||||
common = config.get_common()
|
||||
domain,port = config.get_host()
|
||||
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, apps=apps, common=common, domain=domain, port=port)
|
||||
|
||||
def setup_apps_view(self, request):
|
||||
# Application manager view.
|
||||
repo_error = None
|
||||
try:
|
||||
self.appmgr.pkgmgr.fetch_online_packages() # TODO: fetch is now automatic in @property
|
||||
# Populate online_repo cache or fail early when the repo can't be reached
|
||||
repo_online.get_apps()
|
||||
except InvalidSignature:
|
||||
repo_error = request.session.lang.invalid_packages_signature()
|
||||
except Unauthorized:
|
||||
@ -183,80 +171,97 @@ class WSGIApp:
|
||||
repo_error = request.session.lang.repo_unavailable()
|
||||
table = self.render_setup_apps_table(request)
|
||||
message = self.get_session_message(request)
|
||||
return self.render_html('setup-apps.html', request, repo_error=repo_error, table=table, message=message)
|
||||
repo_url, repo_user, _ = vmmgr.get_repo_settings()
|
||||
common = config.get_common()
|
||||
return self.render_html('setup-apps.html', request, repo_url=repo_url, repo_user=repo_user, repo_error=repo_error, table=table, message=message, common=common)
|
||||
|
||||
def render_setup_apps_table(self, request):
|
||||
lang = request.session.lang
|
||||
online_packages = self.appmgr.pkgmgr.online_packages
|
||||
local_apps = repo_local.get_apps()
|
||||
online_apps = repo_online.get_apps()
|
||||
actionable_apps = sorted(set(online_apps) | set(local_apps))
|
||||
pending_actions = self.queue.get_actions()
|
||||
actionable_apps = sorted(set([k for k, v in online_packages.items() if 'title' in v] + list(self.conf['apps'].keys())))
|
||||
app_data = {}
|
||||
for app in actionable_apps:
|
||||
installed = app in self.conf['packages'] and app in self.conf['apps']
|
||||
title = self.conf['packages'][app]['title'] if installed else online_packages[app]['title']
|
||||
visible = self.conf['apps'][app]['visible'] if installed else False
|
||||
autostarted = self.appmgr.is_service_autostarted(app) if installed else False
|
||||
installed = app in local_apps
|
||||
title = local_apps[app]['meta']['title'] if installed else online_apps[app]['meta']['title']
|
||||
try:
|
||||
visible = local_apps[app]['visible']
|
||||
except:
|
||||
visible = False
|
||||
try:
|
||||
autostarted = local_apps[app]['autostart']
|
||||
except:
|
||||
autostarted = 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())
|
||||
# Display queued or currently processed actions
|
||||
app_queue = pending_actions[app]
|
||||
if app_queue.index:
|
||||
if app_queue.exception:
|
||||
# Display failed task
|
||||
if isinstance(app_queue.exception, InvalidSignature):
|
||||
status = lang.repo_package_invalid_signature()
|
||||
elif isinstance(app_queue.exception, NotFound):
|
||||
status = lang.repo_package_missing()
|
||||
elif isinstance(app_queue.exception, BaseException):
|
||||
if app_queue.action in (vmmgr.start_app, vmmgr.stop_app):
|
||||
status = lang.stop_start_error()
|
||||
else:
|
||||
status = lang.package_manager_error()
|
||||
status = f'<span class="error">{status}<span> <a href="#" class="app-clear-status">OK</a>'
|
||||
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, InvalidSignature):
|
||||
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.repo_package_invalid_signature())
|
||||
actions = None
|
||||
elif isinstance(item.data, NotFound):
|
||||
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.repo_package_missing())
|
||||
actions = None
|
||||
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: #Stage.DOWNLOAD:
|
||||
status = '{} ({} %)'.format(lang.status_downloading(), item.data.percent_processed)
|
||||
elif item.data.stage == 1: #Stage.UNPACK:
|
||||
status = lang.status_unpacking()
|
||||
elif item.data.stage == 2: #Stage.INSTALL_DEPS:
|
||||
status = lang.status_installing_deps()
|
||||
# Display task/subtask progress
|
||||
if app_queue.action == vmmgr.start_app:
|
||||
status = lang.status_starting()
|
||||
elif app_queue.action == vmmgr.stop_app:
|
||||
status = lang.status_stopping()
|
||||
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()
|
||||
action_item = app_queue.queue[app_queue.index-1]
|
||||
if action_item.type in (ActionItemType.IMAGE_DOWNLOAD, ActionItemType.APP_DOWNLOAD):
|
||||
status = lang.status_downloading(action_item.key)
|
||||
elif action_item.type in (ActionItemType.IMAGE_UNPACK, ActionItemType.APP_UNPACK):
|
||||
status = lang.status_unpacking(action_item.key)
|
||||
elif action_item.type == ActionItemType.IMAGE_DELETE:
|
||||
status = lang.status_deleting(action_item.key)
|
||||
elif action_item.type == ActionItemType.APP_INSTALL:
|
||||
status = lang.status_installing(action_item.key)
|
||||
elif action_item.type == ActionItemType.APP_UPDATE:
|
||||
status = lang.status_updating(action_item.key)
|
||||
elif action_item.type == ActionItemType.APP_UNINSTALL:
|
||||
status = lang.status_uninstalling(action_item.key)
|
||||
status = f'[{app_queue.index}/{len(app_queue.queue)}] {status}'
|
||||
if action_item.show_progress:
|
||||
status = f'{status} ({floor(current_action.units_done/current_action.units_total*100)} %)'
|
||||
actions = '<div class="loader"></div>'
|
||||
else:
|
||||
# Display queued (pending, not started) task
|
||||
if app_queue.action == vmmgr.start_app:
|
||||
status = lang.status_starting()
|
||||
elif app_queue.action == vmmgr.stop_app:
|
||||
status = lang.status_stopping()
|
||||
elif app_queue.action == vmmgr.install_app:
|
||||
status = lang.status_installing('')
|
||||
elif app_queue.action == vmmgr.uninstall_app:
|
||||
status = lang.status_uninstalling('')
|
||||
elif app_queue.action == vmmgr.update_app:
|
||||
status = lang.status_updating('')
|
||||
status = f'{status} ({lang.status_queued()})'
|
||||
actions = '<div class="loader"></div>'
|
||||
else:
|
||||
# Diplay apps with no queued or currently processed action
|
||||
if not installed:
|
||||
status = lang.status_not_installed()
|
||||
actions = '<a href="#" class="app-install">{}</a>'.format(lang.action_install())
|
||||
actions = f'<a href="#" class="app-install">{lang.action_install()}</a>'
|
||||
else:
|
||||
if self.appmgr.is_service_started(app):
|
||||
status = '<span class="info">{}</span>'.format(lang.status_started())
|
||||
actions = '<a href="#" class="app-stop">{}</a>'.format(lang.action_stop())
|
||||
if vmmgr.is_app_started(app):
|
||||
status = f'<span class="info">{lang.status_started()}</span>'
|
||||
actions = f'<a href="#" class="app-stop">{lang.action_stop()}</a>'
|
||||
else:
|
||||
status = '<span class="error">{}</span>'.format(lang.status_stopped())
|
||||
actions = '<a href="#" class="app-start">{}</a>, <a href="#" class="app-uninstall">{}</a>'.format(lang.action_start(), lang.action_uninstall())
|
||||
if self.appmgr.pkgmgr.has_update(app):
|
||||
actions = '{}, <a href="#" class="app-update">{}</a>'.format(actions, lang.action_update())
|
||||
status = f'<span class="error">{lang.status_stopped()}</span>'
|
||||
actions = f'<a href="#" class="app-start">{lang.action_start()}</a>, <a href="#" class="app-uninstall">{lang.action_uninstall()}</a>'
|
||||
if parse_version(online_apps[app]['version']) > parse_version(app.version):
|
||||
actions = f'{actions}, <a href="#" class="app-update">{lang.action_update()}</a>'
|
||||
app_data[app] = {'title': title, 'visible': visible, 'installed': installed, 'autostarted': autostarted, 'status': status, 'actions': actions}
|
||||
return self.render_template('setup-apps-table.html', request, app_data=app_data)
|
||||
|
||||
@ -276,15 +281,16 @@ class WSGIApp:
|
||||
return self.render_json({'error': request.session.lang.invalid_domain()})
|
||||
if not validator.is_valid_port(port):
|
||||
return self.render_json({'error': request.session.lang.invalid_port()})
|
||||
self.vmmgr.update_host(domain, port)
|
||||
url = '{}/setup-host'.format(net.compile_url(net.get_local_ip(), port))
|
||||
vmmgr.update_host(domain, port)
|
||||
url = f'{net.compile_url(net.get_local_ip(), port)}/setup-host'
|
||||
response = self.render_json({'ok': request.session.lang.host_updated(url, url)})
|
||||
response.call_on_close(self.vmmgr.restart_nginx)
|
||||
response.call_on_close(vmmgr.restart_nginx)
|
||||
return response
|
||||
|
||||
def verify_dns_action(self, request):
|
||||
# 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']]
|
||||
domain = config.get_host()[0]
|
||||
domains = [domain]+[f'{definition["host"]}.{domain}' for app,definition in config.get_apps().items()]
|
||||
ipv4 = net.get_external_ip(4)
|
||||
ipv6 = net.get_external_ip(6)
|
||||
for domain in domains:
|
||||
@ -304,8 +310,9 @@ class WSGIApp:
|
||||
def verify_http_action(self, request, **kwargs):
|
||||
# Check if all applications are accessible from the internet using 3rd party ping service
|
||||
proto = kwargs['proto']
|
||||
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']]
|
||||
domain, port = config.get_host()
|
||||
port = port if proto == 'https' else '80'
|
||||
domains = [domain]+[f'{definition["host"]}.{domain}' for app,definition in config.get_apps().items()]
|
||||
for domain in domains:
|
||||
url = net.compile_url(domain, port, proto)
|
||||
try:
|
||||
@ -318,9 +325,9 @@ class WSGIApp:
|
||||
def update_cert_action(self, request):
|
||||
# Update certificate - either request via Let's Encrypt or manually upload files
|
||||
if request.form['method'] == 'selfsigned':
|
||||
self.vmmgr.create_selfsigned_cert()
|
||||
vmmgr.create_selfsigned_cert()
|
||||
elif request.form['method'] == 'automatic':
|
||||
self.vmmgr.request_acme_cert()
|
||||
vmmgr.request_acme_cert()
|
||||
elif request.form['method'] == 'manual':
|
||||
if not request.files['public']:
|
||||
return self.render_json({'error': request.session.lang.cert_file_missing()})
|
||||
@ -328,70 +335,71 @@ class WSGIApp:
|
||||
return self.render_json({'error': request.session.lang.key_file_missing()})
|
||||
request.files['public'].save('/tmp/public.pem')
|
||||
request.files['private'].save('/tmp/private.pem')
|
||||
self.vmmgr.install_manual_cert('/tmp/public.pem', '/tmp/private.pem')
|
||||
vmmgr.install_manual_cert('/tmp/public.pem', '/tmp/private.pem')
|
||||
os.unlink('/tmp/public.pem')
|
||||
os.unlink('/tmp/private.pem')
|
||||
else:
|
||||
return self.render_json({'error': request.session.lang.cert_request_error()})
|
||||
url = net.compile_url(self.vmmgr.domain, self.vmmgr.port)
|
||||
url = net.compile_url(*config.get_host())
|
||||
return self.render_json({'ok': request.session.lang.cert_installed(url, url)})
|
||||
|
||||
def update_common_action(self, request):
|
||||
# Update common settings shared between apps - admin e-mail address, Google Maps API key
|
||||
email = request.form['email']
|
||||
if not validator.is_valid_email(email):
|
||||
request.session['msg'] = 'common:error:{}'.format(request.session.lang.invalid_email(email))
|
||||
request.session['msg'] = f'common:error:{request.session.lang.invalid_email(email)}'
|
||||
else:
|
||||
self.vmmgr.update_common_settings(email, request.form['gmaps-api-key'])
|
||||
request.session['msg'] = 'common:info:{}'.format(request.session.lang.common_updated())
|
||||
vmmgr.update_common_settings(email, request.form['gmaps-api-key'])
|
||||
request.session['msg'] = f'common:info:{request.session.lang.common_updated()}'
|
||||
return redirect('/setup-apps')
|
||||
|
||||
def update_repo_action(self, request):
|
||||
# Update repository URL and credentials
|
||||
url = request.form['repourl']
|
||||
if not validator.is_valid_repo_url(url):
|
||||
request.session['msg'] = 'repo:error:{}'.format(request.session.lang.invalid_url(url))
|
||||
request.session['msg'] = f'repo:error:{request.session.lang.invalid_url(url)}'
|
||||
else:
|
||||
self.appmgr.update_repo_settings(url, request.form['repousername'], request.form['repopassword'])
|
||||
request.session['msg'] = 'repo:info:{}'.format(request.session.lang.repo_updated())
|
||||
vmmgr.update_repo_settings(url, request.form['repousername'], request.form['repopassword'])
|
||||
request.session['msg'] = f'repo:info:{request.session.lang.repo_updated()}'
|
||||
return redirect('/setup-apps')
|
||||
|
||||
def update_app_visibility_action(self, request):
|
||||
# Update application visibility on portal page
|
||||
self.appmgr.update_app_visibility(request.form['app'], request.form['value'] == 'true')
|
||||
vmmgr.update_app_visibility(request.form['app'], request.form['value'] == 'true')
|
||||
return self.render_json({'ok': 'ok'})
|
||||
|
||||
def update_app_autostart_action(self, request):
|
||||
# Update value determining if the app should be automatically started after VM boot
|
||||
self.appmgr.update_app_autostart(request.form['app'], request.form['value'] == 'true')
|
||||
vmmgr.update_app_autostart(request.form['app'], request.form['value'] == 'true')
|
||||
return self.render_json({'ok': 'ok'})
|
||||
|
||||
def enqueue_app_action(self, request, action):
|
||||
# Common method for enqueuing app actions
|
||||
self.queue.enqueue_action(request.form['app'], action)
|
||||
app = request.form['app']
|
||||
self.queue.enqueue_action(app, action)
|
||||
response = self.render_json({'ok': self.render_setup_apps_table(request)})
|
||||
response.call_on_close(self.queue.process_actions)
|
||||
return response
|
||||
|
||||
def start_app_action(self, request):
|
||||
# Queues application start along with its dependencies
|
||||
return self.enqueue_app_action(request, self.appmgr.start_app)
|
||||
return self.enqueue_app_action(request, vmmgr.start_app)
|
||||
|
||||
def stop_app_action(self, request):
|
||||
# Queues application stop along with its dependencies
|
||||
return self.enqueue_app_action(request, self.appmgr.stop_app)
|
||||
return self.enqueue_app_action(request, vmmgr.stop_app)
|
||||
|
||||
def install_app_action(self, request):
|
||||
# Queues application installation
|
||||
return self.enqueue_app_action(request, self.appmgr.install_app)
|
||||
return self.enqueue_app_action(request, vmmgr.install_app)
|
||||
|
||||
def uninstall_app_action(self, request):
|
||||
# Queues application uninstallation
|
||||
return self.enqueue_app_action(request, self.appmgr.uninstall_app)
|
||||
return self.enqueue_app_action(request, vmmgr.uninstall_app)
|
||||
|
||||
def update_app_action(self, request):
|
||||
# Queues application update
|
||||
return self.enqueue_app_action(request, self.appmgr.update_app)
|
||||
return self.enqueue_app_action(request, vmmgr.update_app)
|
||||
|
||||
def get_app_status_action(self, request):
|
||||
# Gets application and queue status
|
||||
@ -410,7 +418,7 @@ class WSGIApp:
|
||||
if request.form['newpassword'] == '':
|
||||
return self.render_json({'error': request.session.lang.password_empty()})
|
||||
# No need to explicitly validate old password, update_luks_password will raise exception if it's wrong
|
||||
self.vmmgr.update_password(request.form['oldpassword'], request.form['newpassword'])
|
||||
vmmgr.update_password(request.form['oldpassword'], request.form['newpassword'])
|
||||
except:
|
||||
return self.render_json({'error': request.session.lang.bad_password()})
|
||||
return self.render_json({'ok': request.session.lang.password_changed()})
|
||||
@ -418,42 +426,34 @@ class WSGIApp:
|
||||
def reboot_vm_action(self, request):
|
||||
# Reboots VM
|
||||
response = self.render_json({'ok': request.session.lang.reboot_initiated()})
|
||||
response.call_on_close(self.vmmgr.reboot_vm)
|
||||
response.call_on_close(vmmgr.reboot_vm)
|
||||
return response
|
||||
|
||||
def shutdown_vm_action(self, request):
|
||||
# Shuts down VM
|
||||
response = self.render_json({'ok': request.session.lang.shutdown_initiated()})
|
||||
response.call_on_close(self.vmmgr.shutdown_vm)
|
||||
response.call_on_close(vmmgr.shutdown_vm)
|
||||
return response
|
||||
|
||||
def reload_config_action(self, request):
|
||||
# Reload configuration (called by vmmgr.register_app())
|
||||
self.conf.load()
|
||||
return Response(status=204)
|
||||
|
||||
def is_app_visible(self, app):
|
||||
return app in self.conf['apps'] and self.conf['apps'][app]['visible'] and self.appmgr.is_service_started(app)
|
||||
|
||||
def update_ssh_keys_action(self, request):
|
||||
# Update authorized_keys file
|
||||
remote.set_authorized_keys(request.form['ssh-keys'].replace('\r', ''))
|
||||
request.session['msg'] = 'ssh:info:{}'.format(request.session.lang.ssh_keys_installed())
|
||||
request.session['msg'] = f'ssh:info:{request.session.lang.ssh_keys_installed()}'
|
||||
return redirect('/setup-remote')
|
||||
|
||||
def update_vpn_action(self, request):
|
||||
# Update WireGuard VPN listen port, virtual IP and peer list
|
||||
ip = request.form['vpn-lip']
|
||||
if not ip.isdigit() or not 0 < int(ip) < 255:
|
||||
request.session['msg'] = 'vpn:error:{}'.format(request.session.lang.invalid_ip())
|
||||
request.session['msg'] = f'vpn:error:{request.session.lang.invalid_ip()}'
|
||||
return redirect('/setup-remote')
|
||||
port = request.form['vpn-port']
|
||||
if not port.isdigit() or not 0 < int(port) < 65536:
|
||||
request.session['msg'] = 'vpn:error:{}'.format(request.session.lang.invalid_port())
|
||||
request.session['msg'] = f'vpn:error:{request.session.lang.invalid_port()}'
|
||||
return redirect('/setup-remote')
|
||||
peers = request.form['vpn-peers'].replace('\r', '')
|
||||
remote.set_wireguard_conf(ip, port, peers)
|
||||
request.session['msg'] = 'vpn:info:{}'.format(request.session.lang.vpn_updated())
|
||||
request.session['msg'] = f'vpn:info:{request.session.lang.vpn_updated()}'
|
||||
return redirect('/setup-remote')
|
||||
|
||||
def generate_vpn_key_action(self, request):
|
||||
|
@ -40,11 +40,11 @@ class WSGILang:
|
||||
'status_started': 'Spuštěna',
|
||||
'status_stopping': 'Zastavuje se',
|
||||
'status_stopped': 'Zastavena',
|
||||
'status_downloading': 'Stahuje se',
|
||||
'status_unpacking': 'Rozbaluje se',
|
||||
'status_installing': 'Instaluje se',
|
||||
'status_installing_deps': 'Instalují se závislosti',
|
||||
'status_uninstalling': 'Odinstalovává se',
|
||||
'status_downloading': 'Stahuje se {}',
|
||||
'status_unpacking': 'Rozbaluje se {}',
|
||||
'status_installing': 'Instaluje se {}',
|
||||
'status_updating': 'Aktualizuje se {}',
|
||||
'status_uninstalling': 'Odinstalovává se {}',
|
||||
'status_not_installed': 'Není nainstalována',
|
||||
'action_start': 'Spustit',
|
||||
'action_stop': 'Zastavit',
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block title %}Cluster NGO{% endblock %}
|
||||
{% block body %}
|
||||
{% if is_app_visible('sahana') %}
|
||||
{% set app = conf['apps']['sahana'] %}
|
||||
{% if 'sahana' in visible_apps %}
|
||||
{% set app = apps['sahana'] %}
|
||||
<div class="portal-box portal-box-double-width">
|
||||
<h2><a href="https://sahana.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN" title="Sahana EDEN">Sahana EDEN</a></h2>
|
||||
<p><strong>Registr kontaktů</strong> asociací, organizací, jednotek zaměstnanců, dobrovolníků, <strong>Registr prostředků</strong>, materiálních zdrojů určených pro činnost v krizových situacích, <strong>logistika</strong> krizového zboží ve skladištích, úkrytech, <strong>organizace lidských zdrojů</strong>, diobrovolníků, <strong>mapová vizualizace</strong> pro lokalizaci a popis krizové události a <strong>mnoho dalších funkcí</strong>.</p>
|
||||
@ -13,8 +13,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('sahana-demo') %}
|
||||
{% set app = conf['apps']['sahana-demo'] %}
|
||||
{% if 'sahana-demo' in visible_apps %}
|
||||
{% set app = apps['sahana-demo'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sahana-demo.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN DEMO" title="Sahana EDEN DEMO">Sahana EDEN DEMO</a></h2>
|
||||
<p>Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.</p>
|
||||
@ -25,8 +25,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('sambro') %}
|
||||
{% set app = conf['apps']['sambro'] %}
|
||||
{% if 'sambro' in visible_apps %}
|
||||
{% set app = apps['sambro'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sambro.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN SAMBRO" title="Sahana EDEN SAMBRO">Sahana EDEN SAMBRO</a></h2>
|
||||
<p>Samostatná instance Sahana EDEN s šablonou SAMBRO.</p>
|
||||
@ -48,8 +48,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('crisiscleanup') %}
|
||||
{% set app = conf['apps']['crisiscleanup'] %}
|
||||
{% if 'crisiscleanup' in visible_apps %}
|
||||
{% set app = apps['crisiscleanup'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://cc.{{ host }}"><img src="static/img/Crisis_Cleanup.png" alt="Crisis Cleanup" title="Crisis Cleanup">Crisis Cleanup</a></h2>
|
||||
<p><strong>Mapování krizové pomoci</strong> při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.</p>
|
||||
@ -60,8 +60,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('ckan') %}
|
||||
{% set app = conf['apps']['ckan'] %}
|
||||
{% if 'ckan' in visible_apps %}
|
||||
{% set app = apps['ckan'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://ckan.{{ host }}"><img src="static/img/CKAN.png" alt="CKAN" title="CKAN">CKAN</a></h2>
|
||||
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
|
||||
@ -72,8 +72,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('opendatakit-build') %}
|
||||
{% set app = conf['apps']['opendatakit-build'] %}
|
||||
{% if 'opendatakit-build' in visible_apps %}
|
||||
{% set app = apps['opendatakit-build'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://odkbuild.{{ host }}"><img src="static/img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Build</a></h2>
|
||||
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů<br>
|
||||
@ -83,8 +83,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('opendatakit') %}
|
||||
{% set app = conf['apps']['opendatakit'] %}
|
||||
{% if 'opendatakit' in visible_apps %}
|
||||
{% set app = apps['opendatakit'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="#"><img src="static/img/ODK_Collect.png" alt="Open Data Kit" title="Open Data Kit">ODK Collect</a></h2>
|
||||
<p>Mobilní aplikace<br>
|
||||
@ -108,8 +108,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('openmapkit') %}
|
||||
{% set app = conf['apps']['openmapkit'] %}
|
||||
{% if 'openmapkit' in visible_apps %}
|
||||
{% set app = apps['openmapkit'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://omk.{{ host }}"><img src="static/img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit Server</a></h2>
|
||||
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
|
||||
@ -141,8 +141,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('frontlinesms') %}
|
||||
{% set app = conf['apps']['frontlinesms'] %}
|
||||
{% if 'frontlinesms' in visible_apps %}
|
||||
{% set app = apps['frontlinesms'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sms.{{ host }}"><img src="static/img/FrontlineSMS.png" alt="FrontlineSMS" title="FrontlineSMS">FrontlineSMS</a></h2>
|
||||
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
|
||||
@ -160,8 +160,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('seeddms') %}
|
||||
{% set app = conf['apps']['seeddms'] %}
|
||||
{% if 'seeddms' in visible_apps %}
|
||||
{% set app = apps['seeddms'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://dms.{{ host }}"><img src="static/img/SeedDMS.png" alt="SeedDMS" title="SeedDMS">SeedDMS</a></h2>
|
||||
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
|
||||
@ -172,8 +172,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('pandora') %}
|
||||
{% set app = conf['apps']['pandora'] %}
|
||||
{% if 'pandora' in visible_apps %}
|
||||
{% set app = apps['pandora'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://pandora.{{ host }}"><img src="static/img/Pandora.png" alt="Pan.do/ra" title="Pan.do/ra">Pan.do/ra</a></h2>
|
||||
<p><strong>Media management</strong> na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.</p>
|
||||
@ -184,8 +184,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('ushahidi') %}
|
||||
{% set app = conf['apps']['ushahidi'] %}
|
||||
{% if 'ushahidi' in visible_apps %}
|
||||
{% set app = apps['ushahidi'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://ush.{{ host }}"><img src="static/img/Ushahidi.png" alt="Ushahidi" title="Ushahidi">Ushahidi</a></h2>
|
||||
<p>Reakce na krizovou událost. Shromažďujte zprávy od obětí a pracovníků v terénu prostřednictvím SMS, e-mailu, webu, Twitteru.</p>
|
||||
@ -214,8 +214,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('kanboard') %}
|
||||
{% set app = conf['apps']['kanboard'] %}
|
||||
{% if 'kanboard' in visible_apps %}
|
||||
{% set app = apps['kanboard'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://kb.{{ host }}"><img src="static/img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard</a></h2>
|
||||
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
|
||||
@ -237,8 +237,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('cts') %}
|
||||
{% set app = conf['apps']['cts'] %}
|
||||
{% if 'cts' in visible_apps %}
|
||||
{% set app = apps['cts'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://cts.{{ host }}"><img src="static/img/CTS.png" alt="CTS" title="CTS">CTS</a></h2>
|
||||
<p>Logistika hmotné pomoci pro humanitární potřeby.</p>
|
||||
@ -249,8 +249,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('gnuhealth') %}
|
||||
{% set app = conf['apps']['gnuhealth'] %}
|
||||
{% if 'gnuhealth' in visible_apps %}
|
||||
{% set app = apps['gnuhealth'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://gh.{{ host }}/index.html"><img src="static/img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health</a></h2>
|
||||
<p>Zdravotní a nemocniční informační systém.</p>
|
||||
@ -274,8 +274,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('sigmah') %}
|
||||
{% set app = conf['apps']['sigmah'] %}
|
||||
{% if 'sigmah' in visible_apps %}
|
||||
{% set app = apps['sigmah'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sigmah.{{ host }}/sigmah/"><img src="static/img/Sigmah.png" alt="Sigmah" title="Sigmah">Sigmah</a></h2>
|
||||
<p>Rozpočtování získávání finančních prostředků.</p>
|
||||
@ -286,8 +286,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('motech') %}
|
||||
{% set app = conf['apps']['motech'] %}
|
||||
{% if 'motech' in visible_apps %}
|
||||
{% set app = apps['motech'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://motech.{{ host }}/"><img src="static/img/Motech.png" alt="Motech" title="Motech">Motech</a></h2>
|
||||
<p>Integrace zdravotnických a komunikačních služeb.</p>
|
||||
@ -298,8 +298,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('mifosx') %}
|
||||
{% set app = conf['apps']['mifosx'] %}
|
||||
{% if 'mifosx' in visible_apps %}
|
||||
{% set app = apps['mifosx'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://mifosx.{{ host }}/"><img src="static/img/MifosX.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
|
||||
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</p>
|
||||
@ -321,8 +321,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('odoo') %}
|
||||
{% set app = conf['apps']['odoo'] %}
|
||||
{% if 'odoo' in visible_apps %}
|
||||
{% set app = apps['odoo'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://odoo.{{ host }}/"><img src="static/img/Odoo.png" alt="Odoo" title="Odoo">Odoo</a></h2>
|
||||
<p>Sada aplikací pro správu organizace.</p>
|
||||
@ -334,7 +334,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if false %}
|
||||
{% set app = conf['apps']['diaspora'] %}
|
||||
{% set app = apps['diaspora'] %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="#"><img src="static/img/Diaspora.png" alt="diaspora*" title="diaspora*">diaspora*</a></h2>
|
||||
<p>Autonomní sociání síť s možností propojení do cizích sociálních sítí.</p>
|
||||
|
@ -1,119 +1,119 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block title %}Cluster NGO{% endblock %}
|
||||
{% block body %}
|
||||
{% if is_app_visible('sahana-demo') %}
|
||||
{% if 'sahana-demo' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sahana-demo.{{ host }}/eden/">Řízení humanítární činnosti</a></h2>
|
||||
<p>Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('sambro') %}
|
||||
{% if 'sambro' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sambro.{{ host }}/eden/">Centrum hlášení a výstrah</a></h2>
|
||||
<p>Samostatná instance s šablonou pro centrum hlášení a výstrah.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('crisiscleanup') %}
|
||||
{% if 'crisiscleanup' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://cc.{{ host }}">Mapování následků katastrof</a></h2>
|
||||
<p><strong>Mapování krizové pomoci</strong> při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('ckan') %}
|
||||
{% if 'ckan' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://ckan.{{ host }}">Datový sklad</a></h2>
|
||||
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('opendatakit-build') %}
|
||||
{% if 'opendatakit-build' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://odkbuild.{{ host }}">Sběr formulářových dat</a></h2>
|
||||
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('opendatakit') %}
|
||||
{% if 'opendatakit' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://odk.{{ host }}/">Sběr formulářových dat</a></h2>
|
||||
<p><strong>Sběr dat s pomocí smartphone</strong>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('openmapkit') %}
|
||||
{% if 'openmapkit' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://omk.{{ host }}">Sběr mapových dat</a></h2>
|
||||
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('frontlinesms') %}
|
||||
{% if 'frontlinesms' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sms.{{ host }}">Hromadné odesílání zpráv</a></h2>
|
||||
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('seeddms') %}
|
||||
{% if 'seeddms' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://dms.{{ host }}">Archiv dokumentace</a></h2>
|
||||
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('pandora') %}
|
||||
{% if 'pandora' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://pandora.{{ host }}">Archiv medií</a></h2>
|
||||
<p><strong>Media management</strong> na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('ushahidi') %}
|
||||
{% if 'ushahidi' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://ush.{{ host }}">Skupinová reakce na události</a></h2>
|
||||
<p>Reakce na krizovou událost. Shromažďujte zprávy od obětí a pracovníků v terénu prostřednictvím SMS, e-mailu, webu, Twitteru.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('kanboard') %}
|
||||
{% if 'kanboard' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://kb.{{ host }}">Kanban řízení projektů</a></h2>
|
||||
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('gnuhealth') %}
|
||||
{% if 'gnuhealth' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://gh.{{ host }}/index.html">Lékařské záznamy pacientů</a></h2>
|
||||
<p>Zdravotní a nemocniční informační systém.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('sigmah') %}
|
||||
{% if 'sigmah' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://sigmah.{{ host }}/sigmah/">Finanční řízení sbírek</a></h2>
|
||||
<p>Rozpočtování získávání finančních prostředků.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('motech') %}
|
||||
{% if 'motech' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://motech.{{ host }}/">Automatizace komunikace</a></h2>
|
||||
<p>Integrace zdravotnických a komunikačních služeb.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('mifosx') %}
|
||||
{% if 'mifosx' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://mifosx.{{ host }}/">Mikrofinancování rozvojových projektů</a></h2>
|
||||
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_app_visible('odoo') %}
|
||||
{% if 'odoo' in visible_apps %}
|
||||
<div class="portal-box">
|
||||
<h2><a href="https://odoo.{{ host }}/">Správa organizace</a></h2>
|
||||
<p>Sada aplikací pro správu organizace.</p>
|
||||
|
@ -26,11 +26,11 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>URL serveru:</td>
|
||||
<td><input type="text" name="repourl" value="{{ conf['repo']['url'] }}"></td>
|
||||
<td><input type="text" name="repourl" value="{{ repo_url }}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Uživatelské jméno:</td>
|
||||
<td><input type="text" name="repousername" value="{{ conf['repo']['user'] }}"></td>
|
||||
<td><input type="text" name="repousername" value="{{ repo_user }}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Heslo:</td>
|
||||
@ -56,12 +56,12 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>E-mail</td>
|
||||
<td><input type="text" name="email" value="{{ conf['common']['email'] }}"></td>
|
||||
<td><input type="text" name="email" value="{{ common['email'] }}"></td>
|
||||
<td class="remark">Administrativní e-mail na který budou doručovány zprávy a upozornění z aplikací. Stejná e-mailová adresa bude také využita některými aplikacemi pro odesílání zpráv uživatelům.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Google Maps API klíč</td>
|
||||
<td><input type="text" name="gmaps-api-key" value="{{ conf['common']['gmaps-api-key'] }}"></td>
|
||||
<td><input type="text" name="gmaps-api-key" value="{{ common['gmaps-api-key'] }}"></td>
|
||||
<td class="remark">API klíč pro službu Google Maps, která je využita některými aplikacemi.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -8,12 +8,12 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>Doména</td>
|
||||
<td><input type="text" name="domain" id="domain" value="{{ conf['host']['domain'] }}"></td>
|
||||
<td><input type="text" name="domain" id="domain" value="{{ domain }}"></td>
|
||||
<td class="remark">Plně kvalifikovaný doménový název, na kterém bude dostupný aplikační portál. Jednotlivé aplikace budou dostupné na subdoménách této domény.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port</td>
|
||||
<td><input type="text" name="port" id="port" value="{{ conf['host']['port'] }}"></td>
|
||||
<td><input type="text" name="port" id="port" value="{{ port }}"></td>
|
||||
<td class="remark">HTTPS port na kterém budou dostupné aplikace. Porty 22, 25, 80 a 8080 jsou vyhrazeny k jiným účelům. Výchozí HTTPS port je 443.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -32,14 +32,14 @@
|
||||
<p>Na jmenném serveru domény nastavené v sekci <em>HTTPS Hostitel</em> nastavte DNS záznamy typu A, případně i AAAA pro následující doménové názvy a nasměrujte je na vnější (tj. dostupnou z internetu) IP adresu tohoto virtuální stroje. Toto nastavení lze obvykle provést skrze webové rozhraní registrátora domény.</p>
|
||||
<p>Vnější IPv4 {% if ex_ipv4 %}je <strong>{{ ex_ipv4 }}</strong>{% else %}nebyla zjištěna{% endif %} a IPv6 {% if ex_ipv6 %}je <strong>{{ ex_ipv6 }}</strong>{% else %}nebyla zjištěna{% endif %}.</p>
|
||||
<ul>
|
||||
<li>{{ conf['host']['domain'] }}</li>
|
||||
<li>*.{{ conf['host']['domain'] }}</li>
|
||||
<li>{{ domain }}</li>
|
||||
<li>*.{{ domain }}</li>
|
||||
</ul>
|
||||
<p>Pokud jmenný server nepodporuje wildcard záznamy nebo pokud nemůžete či nechcete dedikovat virtuálnímu stroji všechny subdomény, nastavte místo toho záznamy pro následující doménové názvy</p>
|
||||
<ul style="column-count:3">
|
||||
<li>{{ conf['host']['domain'] }}</li>
|
||||
{% for app in conf['apps']|sort %}
|
||||
<li>{{ conf['apps'][app]['host'] }}.{{ conf['host']['domain'] }}</li>
|
||||
<li>{{ domain }}</li>
|
||||
{% for app in apps|sort %}
|
||||
<li>{{ apps[app]['host'] }}.{{ domain }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="button" id="verify-dns" value="Ověřit nastavení DNS">
|
||||
@ -52,14 +52,14 @@
|
||||
|
||||
<div class="setup-box">
|
||||
<h2>Firewall a NAT</h2>
|
||||
<p>Pokud je stávající připojení k internetu zprostředkováno routerem s NAT, na hypervizoru je nastaven firewall nebo existují jiné restrikce síťového provozu, je nutno upravit nastavení příslušných komponent, aby byl provoz na portu {{ conf['host']['port'] }} (nastaveném v sekci <em>HTTPS Hostitel</em>) z internetu korektně nasměrován na místní adresu virtuálního stroje.</p>
|
||||
<p>Pokud je stávající připojení k internetu zprostředkováno routerem s NAT, na hypervizoru je nastaven firewall nebo existují jiné restrikce síťového provozu, je nutno upravit nastavení příslušných komponent, aby byl provoz na portu {{ port }} (nastaveném v sekci <em>HTTPS Hostitel</em>) z internetu korektně nasměrován na místní adresu virtuálního stroje.</p>
|
||||
<p>Pokud bude využit systém automatického vyžádání a obnovy certifikátu (sekce <em>HTTPS certifikát</em>), je nutno aby byl na místní adresu virtuálního stroje nasměrován i port 80, případně byla nastavena HTTP proxy přesměrovávající doménová jména zmíněná v sekci <em>DNS záznamy</em>.</p>
|
||||
<p>Místní IPv4 {% if in_ipv4 %}je <strong>{{ in_ipv4 }}</strong>{% else %}nebyla zjištěna{% endif %} a IPv6 {% if in_ipv6 %}je <strong>{{ in_ipv6 }}</strong>{% else %}nebyla zjištěna{% endif %}.</p>
|
||||
<input type="button" id="verify-https" value="Ověřit nastavení portu {{ conf['host']['port'] }}">
|
||||
<input type="button" id="verify-https" value="Ověřit nastavení portu {{ port }}">
|
||||
<div id="https-message"></div>
|
||||
<div id="https-wait" class="loader-wrap">
|
||||
<div class="loader"></div>
|
||||
<span>Ověřuje se nastavení firewallu a NAT pro port {{ conf['host']['port'] }}, prosím čekejte...</span>
|
||||
<span>Ověřuje se nastavení firewallu a NAT pro port {{ port }}, prosím čekejte...</span>
|
||||
</div>
|
||||
<input type="button" id="verify-http" value="Ověřit nastavení portu 80">
|
||||
<div id="http-message"></div>
|
||||
|
Loading…
Reference in New Issue
Block a user