Move vmmgr to separate app- directory

This commit is contained in:
Disassembler 2018-11-01 10:10:35 +01:00
commit aaacdd165c
57 changed files with 2579 additions and 0 deletions

19
config.default.json Normal file
View File

@ -0,0 +1,19 @@
{
"apps": {},
"common": {
"email": "admin@example.com",
"gmaps-api-key": ""
},
"host": {
"adminpwd": "${ADMINPWD}",
"domain": "spotter.vm",
"firstrun": true,
"port": "443"
},
"packages": {},
"repo": {
"pwd": "",
"url": "https://dl.dasm.cz/spotter-repo",
"user": ""
}
}

364
mgr/__init__.py Normal file
View File

@ -0,0 +1,364 @@
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
from . import tools
from . import validator
from .config import Config
VERSION = '0.0.1'
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
ACME_CRON = '/etc/periodic/daily/acme-sh'
CERT_PUB_FILE = '/etc/ssl/services.pem'
CERT_KEY_FILE = '/etc/ssl/services.key'
CERT_SAN_FILE = '/etc/ssl/san.cnf'
NGINX_TEMPLATE = '''server {{
listen [::]:{port} ssl http2;
server_name {host}.{domain};
access_log /var/log/nginx/{app}.access.log;
error_log /var/log/nginx/{app}.error.log;
location / {{
proxy_pass http://{app}:8080;
}}
error_page 502 /502.html;
location = /502.html {{
root /srv/vm/templates;
}}
location = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-pong";
}}
}}
'''
NGINX_DEFAULT_TEMPLATE = '''server {{
listen [::]:80 default_server ipv6only=off;
location / {{
return 301 https://$host:{port}$request_uri;
}}
location /.well-known/acme-challenge/ {{
root /etc/acme.sh.d;
}}
location = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-pong";
}}
}}
server {{
listen [::]:{port} ssl http2 default_server ipv6only=off;
location / {{
proxy_pass http://127.0.0.1:8080;
}}
location /static {{
root /srv/vm;
}}
error_page 502 /502.html;
location = /502.html {{
root /srv/vm/templates;
}}
location = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-pong";
}}
}}
'''
ISSUE_TEMPLATE = '''
\x1b[1;32m _____ _ _ __ ____ __
/ ____| | | | | \\\\ \\\\ / / \\\\/ |
| (___ _ __ ___ | |_| |_ ___ _ _\\\\ \\\\ / /| \\\\ / |
\\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| |
____) | |_) | (_) | |_| || __/ | \\\\ / | | | |
|_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_| \\\\/ |_| |_|
| |
|_|\x1b[0m
\x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán.
Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení.
Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít
za následek občanské nebo trestní sankce.
\x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited.
You must have explicit, authorized permission to access or configure this
device. Unauthorized attempts and actions to access or use this system may
result in civil or criminal penalties.
Pro přístup k aplikacím otevřete URL \x1b[1m{url}\x1b[0m ve Vašem
internetovém prohlížeči.
\x1b[0;30m
'''
ACME_CRON_TEMPLATE = '''#!/bin/sh
[ -x /usr/bin/acme.sh ] && /usr/bin/acme.sh --cron >/dev/null
'''
CERT_SAN = '''[ req ]
distinguished_name = dn
x509_extensions = ext
[ dn ]
[ ext ]
subjectAltName=DNS:{domain},DNS:*.{domain}"
'''
class VMMgr:
def __init__(self):
# Load JSON configuration
self.conf = Config()
self.domain = self.conf['host']['domain']
self.port = self.conf['host']['port']
def update_login(self, app, login, password):
# Update login and password for an app in the configuration
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
if login is not None:
self.conf['apps'][app]['login'] = login
if password is not None:
self.conf['apps'][app]['password'] = password
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):
# Extract the variables from values given via lxc.hook.pre-start hook
app = os.environ['LXC_NAME']
# Remove ephemeral layer data
tools.clean_ephemeral_layer(app)
# Configure host and common params used in the app
self.configure_app(app)
def register_container(self):
# Extract the variables from values given via lxc.hook.start-host hook
app = os.environ['LXC_NAME']
pid = os.environ['LXC_PID']
# Lease the first unused IP to the container
ip = tools.get_unused_ip()
tools.update_hosts_lease(ip, app)
tools.set_container_ip(pid, ip)
def unregister_container(self):
# Extract the variables from values given via lxc.hook.post-stop hook
app = os.environ['LXC_NAME']
# Release the container IP
tools.update_hosts_lease(None, app)
# Remove ephemeral layer data
tools.clean_ephemeral_layer(app)
def configure_app(self, app):
script = os.path.join('/srv', app, 'update-conf.sh')
if os.path.exists(script):
setup_env = os.environ.copy()
setup_env['DOMAIN'] = self.domain
setup_env['PORT'] = self.port
setup_env['EMAIL'] = self.conf['common']['email']
setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key']
subprocess.run([script], env=setup_env, check=True)
def register_proxy(self, app):
# 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:
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], domain=self.domain, port=self.port))
tools.reload_nginx()
def unregister_proxy(self, app):
# 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)))
tools.reload_nginx()
def update_host(self, domain, port):
# Update domain and port and rebuild all configuration. Web interface calls tools.restart_nginx() in WSGI close handler
if not validator.is_valid_domain(domain):
raise validator.InvalidValueException('domain', domain)
if not validator.is_valid_port(port):
raise validator.InvalidValueException('port', port)
self.domain = self.conf['host']['domain'] = domain
self.port = self.conf['host']['port'] = port
self.conf.save()
# Restart all apps to trigger configuration refresh
for app in self.conf['apps']:
if tools.is_service_started(app):
tools.restart_service(app)
# Rebuild and restart nginx if it was requested.
self.rebuild_nginx()
def rebuild_nginx(self):
# 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:
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
def rebuild_issue(self):
# Compile the HTTPS host displayed in terminal banner
domain = self.domain
# If the dummy host is used, take an IP address of a primary interface instead
if domain == 'spotter.vm':
domain = tools.get_local_ipv4()
if not domain:
domain = tools.get_local_ipv6()
if not domain:
domain = '127.0.0.1'
# Rebuild the terminal banner
with open(ISSUE_FILE, 'w') as f:
f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(domain, 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):
# Update LUKS password and adminpwd for WSGI application
input = '{}\n{}'.format(oldpassword, newpassword).encode()
subprocess.run(['cryptsetup', 'luksChangeKey', '/dev/sda2'], input=input, check=True)
# Update bcrypt-hashed password in config
self.conf['host']['adminpwd'] = tools.adminpwd_hash(newpassword)
# Save config to file
self.conf.save()
def create_selfsigned_cert(self):
# Remove acme.sh cronjob
if os.path.exists(ACME_CRON):
os.unlink(ACME_CRON)
# Create selfsigned certificate with wildcard alternative subject name
with open(os.path.join(CERT_SAN_FILE), 'w') as f:
f.write(CERT_SAN.format(domain=self.domain))
subprocess.run(['openssl', 'req', '-config', CERT_SAN_FILE, '-x509', '-new', '-out', CERT_PUB_FILE, '-keyout', CERT_KEY_FILE, '-nodes', '-days', '7305', '-subj', '/CN={}'.format(self.domain)], check=True)
os.chmod(CERT_KEY_FILE, 0o640)
def request_acme_cert(self):
# Remove all possible conflicting certificates requested in the past
certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')]
for cert in certs:
if cert != self.domain:
subprocess.run(['/usr/bin/acme.sh', '--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('/etc/acme.sh.d', self.domain)):
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
for app in self.conf['apps']:
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
cmd += ['-w', '/etc/acme.sh.d']
# 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', '--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', '--install-cert', '-d', self.domain, '--key-file', CERT_KEY_FILE, '--fullchain-file', CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
# Install acme.sh cronjob
with open(ACME_CRON, 'w') as f:
f.write(ACME_CRON_TEMPLATE)
def install_manual_cert(self, public_file, private_file):
# Remove acme.sh cronjob
if os.path.exists(ACME_CRON):
os.unlink(ACME_CRON)
# Copy certificate files
shutil.copyfile(public_file, CERT_PUB_FILE)
shutil.copyfile(private_file, CERT_KEY_FILE)
os.chmod(CERT_KEY_FILE, 0o640)
# Reload nginx
tools.reload_nginx()

231
mgr/appmgr.py Normal file
View File

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
import hashlib
import json
import os
import requests
import shutil
import subprocess
import time
import uuid
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from threading import Lock
from . import tools
PUB_FILE = '/srv/vm/packages.pub'
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:
def __init__(self, total):
# Stage 0 = download, 1 = deps install, 2 = app install
self.stage = 0
self.total = total
self.downloaded = 0
def __str__(self):
# Limit the disaplyed percentage between 1 - 99 for aestethical and psychological reasons
return str(max(1, min(99, round(self.downloaded / self.total * 100))))
class AppMgr:
def __init__(self, vmmgr):
self.vmmgr = vmmgr
self.conf = vmmgr.conf
self.online_packages = {}
self.action_queue = {}
self.lock = Lock()
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']), stream=stream)
def fetch_online_packages(self):
# Fetches and verifies online packages. Can raise InvalidSignature
online_packages = {}
packages = self.get_repo_resource('packages').content
packages_sig = self.get_repo_resource('packages.sig').content
with open(PUB_FILE, 'rb') as f:
pub_key = load_pem_public_key(f.read(), default_backend())
pub_key.verify(packages_sig, packages, ec.ECDSA(hashes.SHA512()))
online_packages = json.loads(packages)
# Minimze the time when self.online_packages is out of sync
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):
if not tools.is_service_started(item.app):
self.vmmgr.start_app(item.app)
def stop_app(self, item):
if tools.is_service_started(item.app):
self.vmmgr.stop_app(item.app)
def install_app(self, item):
# 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']]
item.data = InstallItem(sum(self.online_packages[d]['size'] for d in deps))
for dep in deps:
self.download_package(dep, item.data)
for dep in deps:
item.data.stage = 2 if dep == deps[-1] else 1
# Purge old data before unpacking to clean previous failed installation
self.purge_package(dep)
self.unpack_package(dep)
# Run uninstall script before installation to clean previous failed installation
self.run_uninstall_script(dep)
self.register_package(dep)
self.run_install_script(dep)
def uninstall_app(self, item):
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
self.stop_app(item)
if tools.is_service_autostarted(item.app):
self.vmmgr.disable_autostart(item.app)
deps = self.get_install_deps(item.app, False)[::-1]
for dep in deps:
if dep not in self.get_uninstall_deps():
self.run_uninstall_script(dep)
self.purge_package(dep)
self.unregister_package(dep)
def download_package(self, name, installitem):
tmp_archive = '/tmp/{}.tar.xz'.format(name)
r = self.get_repo_resource('{}.tar.xz'.format(name), True)
with open(tmp_archive, 'wb') as f:
for chunk in r.iter_content(chunk_size=65536):
if chunk:
installitem.downloaded += f.write(chunk)
def unpack_package(self, name):
tmp_archive = '/tmp/{}.tar.xz'.format(name)
# Verify hash
if self.online_packages[name]['sha512'] != hash_file(tmp_archive):
raise InvalidSignature(name)
# Unpack
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
os.unlink(tmp_archive)
def purge_package(self, name):
# Removes package and shared data from filesystem
lxcpath = self.conf['packages'][name]['lxcpath'] if name in self.conf['packages'] else self.online_packages[name]['lxcpath']
lxc_dir = os.path.join(LXC_ROOT, lxcpath)
if os.path.exists(lxc_dir):
shutil.rmtree(lxc_dir)
srv_dir = os.path.join('/srv/', name)
if os.path.exists(srv_dir):
shutil.rmtree(srv_dir)
lxc_log = '/var/log/lxc/{}.log'.format(name)
if os.path.exists(lxc_log):
os.unlink(lxc_log)
def register_package(self, name):
# Registers a package in local configuration
metadata = self.online_packages[name]
self.conf['packages'][name] = {
'deps': metadata['deps'],
'lxcpath': metadata['lxcpath'],
'version': metadata['version']
}
# If host definition is present, register the package as application
if 'host' in metadata:
self.conf['apps'][name] = {
'title': metadata['title'],
'host': metadata['host'],
'login': 'N/A',
'password': 'N/A',
'visible': False
}
self.conf.save()
def unregister_package(self, name):
# Removes a package from local configuration
del self.conf['packages'][name]
if name in self.conf['apps']:
del self.conf['apps'][name]
self.conf.save()
def run_install_script(self, name):
# Runs install.sh for a package, if the script is present
install_dir = os.path.join('/srv/', name, 'install')
install_script = os.path.join('/srv/', name, 'install.sh')
if os.path.exists(install_script):
subprocess.run(install_script, check=True)
os.unlink(install_script)
if os.path.exists(install_dir):
shutil.rmtree(install_dir)
def run_uninstall_script(self, name):
# Runs uninstall.sh for a package, if the script is present
uninstall_script = os.path.join('/srv/', name, 'uninstall.sh')
if os.path.exists(uninstall_script):
subprocess.run(uninstall_script, check=True)
def get_install_deps(self, name, online=True):
# Flatten dependency tree for a package while preserving the dependency order
packages = self.online_packages if online else self.conf['packages']
deps = packages[name]['deps'].copy()
for dep in deps[::-1]:
deps[:0] = [d for d in self.get_install_deps(dep, online)]
deps = list(dict.fromkeys(deps + [name]))
return deps
def get_uninstall_deps(self):
# Create reverse dependency tree for all installed packages
deps = {}
for pkg in self.conf['packages']:
for d in self.conf['packages'][pkg]['deps']:
deps.setdefault(d, []).append(pkg)
return deps
def hash_file(file_path):
sha512 = hashlib.sha512()
with open(file_path, 'rb') as f:
while True:
data = f.read(65536)
if not data:
break
sha512.update(data)
return sha512.hexdigest()

24
mgr/config.py Normal file
View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import json
from threading import Lock
CONF_FILE = '/srv/vm/config.json'
class Config:
def __init__(self):
self.lock = Lock()
self.load()
def load(self):
with self.lock:
with open(CONF_FILE, 'r') as f:
self.data = json.load(f)
def save(self):
with self.lock:
with open(CONF_FILE, 'w') as f:
json.dump(self.data, f, sort_keys=True, indent=4)
def __getitem__(self, attr):
return self.data[attr]

159
mgr/tools.py Normal file
View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
import bcrypt
import dns.exception
import dns.resolver
import os
import requests
import shutil
import socket
import subprocess
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
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)
def get_local_ipv4():
# Return first routable IPv4 address of the VM (container host)
try:
return subprocess.run(['/sbin/ip', 'route', 'get', '1'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-1]
except:
return None
def get_local_ipv6():
# Return first routable IPv6 address of the VM (container host)
try:
return subprocess.run(['/sbin/ip', 'route', 'get', '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-3]
except:
return None
def get_external_ip(family):
# Return external IP address of given family via 3rd party service
allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family
try:
requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family
return requests.get('https://tools.dasm.cz/myip.php', timeout=5).text
except:
return None
finally:
requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family
def get_external_ipv4():
# Return external IPv4 address
return get_external_ip(socket.AF_INET)
def get_external_ipv6():
# Return external IPv6 address
return get_external_ip(socket.AF_INET6)
resolver = dns.resolver.Resolver()
resolver.timeout = 3
resolver.lifetime = 3
resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844']
def resolve_ip(domain, type):
# Resolve domain name using Google Public DNS
try:
return resolver.query(domain, type)[0].address
except dns.exception.Timeout:
raise
except:
return None
def ping_url(url):
try:
return requests.get('https://tools.dasm.cz/vm-ping.php', params = {'url': url}, timeout=5).text == 'vm-pong'
except requests.exceptions.Timeout:
raise
except:
return False
def is_service_started(app):
# Check OpenRC service status without calling any binary
return os.path.exists(os.path.join('/run/openrc/started', app))
def is_service_autostarted(app):
# Check OpenRC service enablement
return os.path.exists(os.path.join('/etc/runlevels/default', app))
def start_service(service):
subprocess.run(['/sbin/service', service, 'start'], check=True)
def stop_service(service):
subprocess.run(['/sbin/service', service, 'stop'], check=True)
def restart_service(service):
subprocess.run(['/sbin/service', service, 'restart'])
def reload_nginx():
subprocess.run(['/usr/sbin/nginx', '-s', 'reload'])
def restart_nginx():
restart_service('nginx')
def get_cert_info(cert):
# Gather certificate data important for setup-host
with open(cert, 'rb') as f:
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),
'method': 'manual'}
if os.path.exists('/etc/periodic/daily/acme-sh'):
data['method'] = 'letsencrypt'
# This is really naive method of inferring if the cert is selfsigned and should never be used in production :)
elif data['subject'] == data['issuer']:
data['method'] = 'selfsigned'
return data
def adminpwd_hash(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def adminpwd_verify(password, hash):
return bcrypt.checkpw(password.encode(), hash.encode())
def shutdown_vm():
subprocess.run(['/sbin/poweroff'])
def reboot_vm():
subprocess.run(['/sbin/reboot'])
def get_unused_ip():
# This is a poor man's DHCP server which uses /etc/hosts as lease database
# Leases the first unused IP from range 172.17.0.0/16
leased = []
with open('/etc/hosts', 'r') as f:
for line in f.read().splitlines():
if line.startswith('172.17'):
ip = line.split()[0].split('.')
leased.append(int(ip[2]) * 256 + int(ip[3]))
for i in range(1, 65534):
if i not in leased:
break
return '172.17.{}.{}'. format(i // 256, i % 256)
def update_hosts_lease(ip, app):
hosts = []
with open('/etc/hosts', 'r') as f:
for line in f:
if not line.strip().endswith(' {}'.format(app)):
hosts.append(line)
if ip:
hosts.append('{} {}\n'.format(ip, app))
with open('/etc/hosts', 'w') as f:
f.writelines(hosts)
def set_container_ip(pid, ip):
# Set IP in container based on PID given via lxc.hook.start-host hook
cmd = 'ip addr add {}/16 broadcast 172.17.255.255 dev eth0 && ip route add default via 172.17.0.1'.format(ip)
subprocess.run(['nsenter', '-a', '-t', pid, '--', '/bin/sh', '-c', cmd])
def clean_ephemeral_layer(app):
layer = os.path.join('/var/lib/lxc', app, 'delta0')
if os.path.exists(layer):
for item in os.scandir(layer):
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)

25
mgr/validator.py Normal file
View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import re
domain_re = re.compile(r'^(?!-)[a-z0-9-]{1,63}(?<!-)(?:\.(?!-)[a-z0-9-]{1,63}(?<!-)){0,125}\.(?!-)(?![0-9]+$)[a-z0-9-]{1,63}(?<!-)$')
box_re = re.compile(r'^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*$')
class InvalidValueException(Exception):
pass
def is_valid_domain(domain):
return bool(domain_re.match(domain))
def is_valid_port(port):
try:
port = int(port)
return port > 0 and port < 65536
except:
return False
def is_valid_email(email):
parts = email.split('@')
if len(parts) != 2:
return False
return bool(box_re.match(parts[0])) and bool(domain_re.match(parts[1]))

399
mgr/wsgiapp.py Normal file
View File

@ -0,0 +1,399 @@
# -*- coding: utf-8 -*-
import json
import os
from werkzeug.exceptions import BadRequest, HTTPException, NotFound
from werkzeug.routing import Map, Rule
from werkzeug.utils import redirect
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import ClosingIterator
from jinja2 import Environment, FileSystemLoader
from . import VMMgr, CERT_PUB_FILE
from . import tools
from .appmgr import AppMgr
from .validator import InvalidValueException
from .wsgilang import WSGILang
from .wsgisession import WSGISession
SESSION_KEY = os.urandom(26)
class WSGIApp(object):
def __init__(self):
self.vmmgr = VMMgr()
self.appmgr = AppMgr(self.vmmgr)
self.conf = self.vmmgr.conf
self.jinja_env = Environment(loader=FileSystemLoader('/srv/vm/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_service_autostarted=tools.is_service_autostarted)
self.jinja_env.globals.update(is_service_started=tools.is_service_started)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
request = Request(environ)
# Reload config in case it has changed between requests
self.conf.load()
# Enhance request
request.session = WSGISession(request.cookies, SESSION_KEY)
request.session.lang = WSGILang()
# Dispatch request
response = self.dispatch_request(request)
# Save session if changed
request.session.save(response)
return response(environ, start_response)
def dispatch_request(self, request):
adapter = self.get_url_map(request.session).bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except NotFound as e:
# Return custom 404 page
response = self.render_template('404.html', request)
response.status_code = 404
return response
except HTTPException as e:
return e
def get_url_map(self, session):
rules = [
Rule('/', endpoint='portal_view'),
Rule('/login', methods=['GET'], endpoint='login_view', defaults={'redirect': '/'}),
Rule('/login', methods=['POST'], endpoint='login_action'),
Rule('/logout', endpoint='logout_action')
]
if session['admin']:
rules += [
Rule('/setup-host', endpoint='setup_host_view'),
Rule('/setup-apps', endpoint='setup_apps_view'),
Rule('/update-host', endpoint='update_host_action'),
Rule('/verify-dns', endpoint='verify_dns_action'),
Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}),
Rule('/verify-http', endpoint='verify_http_action', defaults={'proto': 'http'}),
Rule('/update-cert', endpoint='update_cert_action'),
Rule('/update-common', endpoint='update_common_action'),
Rule('/update-repo', endpoint='update_repo_action'),
Rule('/update-app-visibility', endpoint='update_app_visibility_action'),
Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
Rule('/start-app', endpoint='start_app_action'),
Rule('/stop-app', endpoint='stop_app_action'),
Rule('/install-app', endpoint='install_app_action'),
Rule('/get-progress', endpoint='get_progress_action'),
Rule('/uninstall-app', endpoint='uninstall_app_action'),
Rule('/update-password', endpoint='update_password_action'),
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
Rule('/reboot-vm', endpoint='reboot_vm_action'),
]
else:
rules += [
Rule('/setup-host', endpoint='login_view', defaults={'redirect': '/setup-host'}),
Rule('/setup-apps', endpoint='login_view', defaults={'redirect': '/setup-apps'}),
]
return Map(rules)
def render_template(self, template_name, request, **context):
# Enhance context
context['conf'] = self.conf
context['session'] = request.session
# Render template
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
def render_json(self, data):
return Response(json.dumps(data), mimetype='application/json')
def login_view(self, request, **kwargs):
return self.render_template('login.html', request, redirect=kwargs['redirect'])
def login_action(self, request):
password = request.form['password']
redir_url = request.form['redirect']
if tools.adminpwd_verify(password, self.conf['host']['adminpwd']):
request.session['admin'] = True
return redirect(redir_url)
else:
return self.render_template('login.html', request, message=request.session.lang.bad_password())
def logout_action(self, request):
request.session.reset()
return redirect('/')
def portal_view(self, request):
# Default portal view. If this is the first run, perform first-run setup.
if self.conf['host']['firstrun']:
# Set user as admin
request.session['admin'] = True
# Disable and save first-run flag
self.conf['host']['firstrun'] = False
self.conf.save()
# Redirect to host setup view
return redirect('/setup-host')
host = tools.compile_url(self.conf['host']['domain'], self.conf['host']['port'])[8:]
if request.session['admin']:
return self.render_template('portal-admin.html', request, host=host)
return self.render_template('portal-user.html', request, host=host)
def setup_host_view(self, request):
# Host setup view.
ex_ipv4 = tools.get_external_ipv4()
ex_ipv6 = tools.get_external_ipv6()
in_ipv4 = tools.get_local_ipv4()
in_ipv6 = tools.get_local_ipv6()
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)
def setup_apps_view(self, request):
# Application manager view.
try:
self.appmgr.fetch_online_packages()
except:
pass
all_apps = sorted(set([k for k,v in self.appmgr.online_packages.items() if 'host' in v] + list(self.conf['apps'].keys())))
return self.render_template('setup-apps.html', request, all_apps=all_apps, online_packages=self.appmgr.online_packages)
def render_setup_apps_row(self, request, app, app_title, item):
lang = request.session.lang
actions = '<div class="loader"></div>'
if item.action == 'start_app':
if not item.started:
status = 'Spouští se (ve frontě)'
elif not item.finished:
status = 'Spouští se'
elif isinstance(item.data, BaseException):
status = '<span class="error">{}</span>'.format(lang.stop_start_error())
else:
status = '<span class="info">Spuštěna</span>'
actions = '<a href="#" class="app-stop">Zastavit</a>'
elif item.action == 'stop_app':
if not item.started:
status = 'Zastavuje se (ve frontě)'
elif not item.finished:
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:
status = 'Instaluje se'
elif isinstance(item.data, BaseException):
status = '<span class="error">{}</span>'.format(lang.package_manager_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 == '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):
# Update domain and port, then restart nginx
try:
domain = request.form['domain']
port = request.form['port']
self.vmmgr.update_host(domain, port)
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
url = '{}/setup-host'.format(tools.compile_url(server_name, port))
response = self.render_json({'ok': request.session.lang.host_updated(url, url)})
response.call_on_close(tools.restart_nginx)
return response
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
except InvalidValueException as e:
if e.args[0] == 'domain':
return self.render_json({'error': request.session.lang.invalid_domain(domain)})
if e.args[0] == 'port':
return self.render_json({'error': request.session.lang.invalid_port(port)})
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']]
ipv4 = tools.get_external_ipv4()
ipv6 = tools.get_external_ipv6()
for domain in domains:
try:
a = tools.resolve_ip(domain, 'A')
aaaa = tools.resolve_ip(domain, 'AAAA')
if not a and not aaaa:
return self.render_json({'error': request.session.lang.dns_record_does_not_exist(domain)})
if a and a != ipv4:
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, a, ipv4)})
if aaaa and aaaa != ipv6:
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, aaaa, ipv6)})
except:
return self.render_json({'error': request.session.lang.dns_timeout()})
return self.render_json({'ok': request.session.lang.dns_records_ok()})
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']]
for domain in domains:
url = tools.compile_url(domain, port, proto)
try:
if not tools.ping_url(url):
return self.render_json({'error': request.session.lang.http_host_not_reachable(url)})
except:
return self.render_json({'error': request.session.lang.http_timeout()})
return self.render_json({'ok': request.session.lang.http_hosts_ok(port)})
def update_cert_action(self, request):
# Update certificate - either request via Let's Encrypt or manually upload files
try:
if request.form['method'] not in ['selfsigned', 'automatic', 'manual']:
raise BadRequest()
if request.form['method'] == 'selfsigned':
self.vmmgr.create_selfsigned_cert()
elif request.form['method'] == 'automatic':
self.vmmgr.request_acme_cert()
else:
if not request.files['public']:
return self.render_json({'error': request.session.lang.cert_file_missing()})
if not request.files['private']:
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')
os.unlink('/tmp/public.pem')
os.unlink('/tmp/private.pem')
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': request.session.lang.cert_request_error()})
url = tools.compile_url(self.vmmgr.domain, self.vmmgr.port)
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
try:
self.vmmgr.update_common(request.form['email'], request.form['gmaps-api-key'])
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': request.session.lang.common_updated()})
def update_repo_action(self, request):
# Update repository URL and credentials
try:
self.conf['repo']['url'] = request.form['repourl']
self.conf['repo']['user'] = request.form['repousername']
self.conf['repo']['pwd'] = request.form['repopassword']
self.conf.save()
except:
pass
return redirect('/setup-apps')
def update_app_visibility_action(self, request):
# Update application visibility on portal page
try:
if request.form['value'] == 'true':
self.vmmgr.show_tiles(request.form['app'])
else:
self.vmmgr.hide_tiles(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
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
try:
if request.form['value'] == 'true':
self.vmmgr.enable_autostart(request.form['app'])
else:
self.vmmgr.disable_autostart(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def enqueue_action(self, request, action):
try:
app = request.form['app']
except BadRequest:
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']
id,item = self.appmgr.enqueue_action(action, app)
response = self.render_json({'html': self.render_setup_apps_row(request, app, app_title, item), 'id': id})
response.call_on_close(lambda: self.appmgr.process_action(id))
return response
def start_app_action(self, request):
# Queues application start along with its dependencies
return self.enqueue_action(request, 'start_app')
def stop_app_action(self, request):
# Queues application stop along with its dependencies
return self.enqueue_action(request, 'stop_app')
def install_app_action(self, request):
# Queues application installation
return self.enqueue_action(request, 'install_app')
def uninstall_app_action(self, request):
# Queues application uninstallation
return self.enqueue_action(request, 'uninstall_app')
def get_progress_action(self, request):
# Gets appmgr queue status for given ids
json = {}
try:
ids = request.form.getlist('ids[]')
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
actions = self.appmgr.get_actions(ids)
for id,item in actions.items():
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):
# Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account
try:
if request.form['newpassword'] != request.form['newpassword2']:
return self.render_json({'error': request.session.lang.password_mismatch()})
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'])
except:
return self.render_json({'error': request.session.lang.bad_password()})
return self.render_json({'ok': request.session.lang.password_changed()})
def reboot_vm_action(self, request):
# Reboots VM
response = self.render_json({'ok': request.session.lang.reboot_initiated()})
response.call_on_close(tools.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(tools.shutdown_vm)
return response
def is_app_visible(self, app):
return app in self.conf['apps'] and self.conf['apps'][app]['visible'] and tools.is_service_started(app)
class InvalidRecordException(Exception):
pass

35
mgr/wsgilang.py Normal file
View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
class WSGILang:
lang = {
'malformed_request': 'Byl zaslán chybný požadavek. Obnovte stránku a zkuste akci zopakovat.',
'invalid_domain': 'Zadaný doménový název "{}" není platný.',
'invalid_port': 'Zadaný port "{}" není platný.',
'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL <a href="{}">{}</a> a pokračujte následujícími kroky.',
'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.',
'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.',
'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'dns_records_ok': 'DNS záznamy jsou nastaveny správně.',
'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.',
'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'http_hosts_ok': 'Síť je nastavena správně. Všechny aplikace na portu {} jsou z internetu dostupné.',
'cert_file_missing': 'Nebyl vybrán soubor s certifikátem.',
'key_file_missing': 'Nebyl vybrán soubor se soukromým klíčem.',
'cert_request_error': 'Došlo k chybě při žádosti o certifikát. Zkontrolujte, zda je virtuální stroj dostupný z internetu na portu 80.',
'cert_installed': 'Certifikát byl úspěšně nainstalován. Přejděte na URL <a href="{}">{}</a> nebo restartujte webový prohlížeč pro jeho načtení.',
'common_updated': 'Nastavení aplikací bylo úspěšně změněno.',
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.',
'package_manager_error': 'Došlo k chybě při instalaci aplikace. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'bad_password': 'Nesprávné heslo',
'password_mismatch': 'Zadaná hesla se neshodují',
'password_empty': 'Nové heslo nesmí být prázdné',
'password_changed': 'Heslo úspěšně změněno',
'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.',
}
def __getattr__(self, key):
def function(*args):
return self.lang[key].format(*args)
return function

32
mgr/wsgisession.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from werkzeug.contrib.securecookie import SecureCookie
class WSGISession:
def __init__(self, cookies, secret_key):
self.secret_key = secret_key
data = cookies.get('session')
if data:
self.sc = SecureCookie.unserialize(data, secret_key)
else:
self.reset()
if 'admin' not in self.sc:
self.reset()
def __getitem__(self, key):
return self.sc.__getitem__(key)
def __setitem__(self, key, value):
return self.sc.__setitem__(key, value)
def __delitem__(self, key):
return self.sc.__delitem__(key)
def __contains__(self, key):
return self.sc.__contains__(key)
def reset(self):
self.sc = SecureCookie(secret_key=self.secret_key)
self.sc['admin'] = False
def save(self, response):
if self.sc.should_save:
data = self.sc.serialize()
response.set_cookie('session', data, httponly=True)

5
packages.pub Normal file
View File

@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWJXH4Qm0kt2L86sntQH+C1zOJNQ0qMRt
0vx4krTxRs9HQTQYAy//JC92ea2aKleA8OL0JF90b1NYXcQCWdAS+vE/ng9IEAii
8C2+5nfuFeZ5YUjbQhfFblwHSM0c7hEG
-----END PUBLIC KEY-----

200
static/css/style.css Normal file
View File

@ -0,0 +1,200 @@
body {
font-family: 'Calibri', 'Verdana', 'Tahoma', sans-serif;
background-color: #bbb;
color: #000;
line-height: 150%;
margin: 25px 30px;
}
a {
color: #06f;
text-decoration: none;
}
img {
border: 0px;
}
nav {
float: right;
}
nav #menu-button {
float: right;
cursor: pointer;
}
nav #menu-button div {
width: 24px;
height: 4px;
background-color: #fff;
border: 1px solid #000;
margin: 2px 0px;
}
nav ul {
display: none;
list-style: none;
border: 1px solid #000;
margin: 26px 0px 0px 0px;
position: absolute;
background-color: #fff;
padding: 10px;
right: 30px;
z-index: 1;
}
nav a {
display: block;
}
h1, h2 {
font-size: 150%;
}
header {
color: #fff;
}
header h1,
header p,
.portal-box p {
padding: 0px;
margin: 0px;
}
.portal-box,
.setup-box {
background-color: #fff;
margin-top: 13px;
border: solid 1px #000;
padding: 10px;
}
.portal-box {
position: relative;
margin-right: 13px;
width: 365px;
float: left;
height: 175px;
}
.portal-box h2 {
margin: 0px;
font-weight: normal;
}
.portal-box h2 a {
color: inherit;
}
.portal-box h2 img {
float: right;
margin-left: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
.portal-box ul {
margin: 0px;
padding-left: 30px;
}
.portal-box:last-child:after {
clear: both;
}
.portal-box-double-width {
width: 765px;
}
.ico {
margin-right: 5px;
width: 20px;
height: 20px;
vertical-align: top;
}
.setup-box h2 {
margin-top: 0px;
}
.setup-box input[type="text"],
.setup-box input[type="password"],
.setup-box input[type="submit"],
.setup-box input[type="button"],
.setup-box input[type="file"],
.setup-box select {
box-sizing: border-box;
width: 180px;
}
.setup-box table {
border-collapse: collapse;
}
.setup-box thead {
font-weight: bold;
}
.setup-box td {
padding: 1px 10px;
vertical-align: top;
}
.setup-box td:first-child {
white-space: nowrap;
}
.setup-box td.remark {
color: #999;
font-size: 80%;
font-style: italic;
line-height: 125%;
padding-top: 5px;
}
#app-manager {
table-layout: fixed;
}
.center {
text-align: center;
}
.error {
color: #c00;
font-weight: bold;
}
.info {
color: #090;
font-weight: bold;
}
.loader-wrap {
display: none;
}
.loader-wrap span:after {
clear: both;
content: '';
display: table;
}
.loader {
float: left;
width: 14px;
height: 14px;
border: 5px solid #eee;
border-top: 5px solid #fa3;
border-radius: 50%;
margin-right: 5px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

BIN
static/img/CAP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
static/img/CKAN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
static/img/CTS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
static/img/Diaspora.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/img/EDEN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/FrontlineSMS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
static/img/GNU_Health.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
static/img/Kanboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/img/MifosX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
static/img/Motech.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
static/img/ODK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/img/ODK_Collect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
static/img/OMK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
static/img/OpenID.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
static/img/POSM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/img/Pandora.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/img/PostGIS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/img/SMS_Sync.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/img/SeedDMS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/img/Sigmah.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
static/img/Ushahidi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/img/icons/Java.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/img/icons/Linux.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
static/img/icons/MacOS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
static/img/icons/iOS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

229
static/js/admin.js Normal file
View File

@ -0,0 +1,229 @@
var action_queue = [];
$(function() {
$('#update-host').on('submit', update_host);
$('#verify-dns').on('click', verify_dns);
$('#verify-https').on('click', verify_https);
$('#verify-http').on('click', verify_http);
$('#cert-method').on('change', toggle_cert_method);
$('#update-cert').on('submit', update_cert);
$('#update-common').on('submit', update_common);
$('#app-manager')
.on('click', '.app-visible', update_app_visibility)
.on('click', '.app-autostart', update_app_autostart)
.on('click', '.app-start', start_app)
.on('click', '.app-stop', stop_app)
.on('click', '.app-install', install_app)
.on('click', '.app-uninstall', uninstall_app);
$('#update-password').on('submit', update_password);
$('#reboot-vm').on('click', reboot_vm);
$('#shutdown-vm').on('click', shutdown_vm);
window.setInterval(check_progress, 1000);
});
function update_host() {
$('#host-submit').hide();
$('#host-message').hide();
$('#host-wait').show();
$.post('/update-host', {'domain': $('#domain').val(), 'port': $('#port').val()}, function(data) {
$('#host-wait').hide();
if (data.error) {
$('#host-message').attr('class','error').html(data.error).show();
$('#host-submit').show();
} else {
$('#host-message').attr('class','info').html(data.ok).show();
$('input').prop('disabled', true);
$('.setup-box').slice(1).css('opacity', '0.5');
}
});
return false;
}
function verify_dns() {
$('#verify-dns').hide();
$('#dns-message').hide();
$('#dns-wait').show();
$.get('/verify-dns', function(data) {
$('#dns-wait').hide();
if (data.error) {
$('#dns-message').attr('class','error').html(data.error).show();
$('#verify-dns').show();
} else {
$('#dns-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function _verify_http(proto) {
$('#verify-'+proto).hide();
$('#'+proto+'-message').hide();
$('#'+proto+'-wait').show();
$.get('/verify-' + proto, function(data) {
$('#'+proto+'-wait').hide();
if (data.error) {
$('#'+proto+'-message').attr('class','error').html(data.error).show();
$('#verify-'+proto).show();
} else {
$('#'+proto+'-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_http() {
return _verify_http('http');
}
function verify_https() {
return _verify_http('https');
}
function toggle_cert_method() {
if ($('#cert-method').val() == 'manual') {
$('.cert-upload').show();
} else {
$('.cert-upload').hide();
}
}
function update_cert() {
$('#cert-submit').hide();
$('#cert-message').hide();
$('#cert-wait').show();
$.ajax({url: '/update-cert', type: 'POST', data: new FormData($('#update-cert')[0]), cache: false, contentType: false, processData: false, success: function(data) {
$('#cert-wait').hide();
if (data.error) {
$('#cert-message').attr('class','error').html(data.error).show();
$('#cert-submit').show();
} else {
$('#cert-message').attr('class','info').html(data.ok).show();
}
}});
return false;
}
function update_common() {
$('#common-submit').hide();
$('#common-message').hide();
$('#common-wait').show();
$.post('/update-common', {'email': $('#email').val(), 'gmaps-api-key': $('#gmaps-api-key').val()}, function(data) {
$('#common-wait').hide();
if (data.error) {
$('#common-message').attr('class','error').html(data.error).show();
$('#common-submit').show();
} else {
$('#common-message').attr('class','info').html(data.ok).show();
$('#common-submit').show();
}
});
return false;
}
function _update_app(item, ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-'+item, {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function update_app_visibility(ev) {
return _update_app('visibility', ev);
}
function update_app_autostart(ev) {
return _update_app('autostart', ev);
}
function _do_app(action, ev) {
var el = $(ev.target);
var tr = el.closest('tr');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/'+action+'-app', {'app': tr.data('app')}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else if (action) {
tr.html(data.html);
action_queue.push(data.id);
}
});
return false;
}
function start_app(ev) {
return _do_app('start', ev);
}
function stop_app(ev) {
return _do_app('stop', ev);
}
function install_app(ev) {
return _do_app('install', ev);
}
function uninstall_app(ev) {
var app = $(ev.target).closest('tr').children().first().text()
if (confirm('Opravdu chcete odinstalovat aplikaci '+app+'?')) {
return _do_app('uninstall', ev);
}
return false;
}
function check_progress() {
if (action_queue.length) {
$.post('/get-progress', {'ids': action_queue}, function(data) {
for (id in data) {
var app = id.split(':')[0];
$('#app-manager tr[data-app="'+app+'"]').html(data[id].html);
if (data[id].last) {
action_queue = action_queue.filter(function(item) {
return item !== id
});
}
}
});
}
}
function update_password() {
$('#password-submit').hide();
$('#password-message').hide();
$('#password-wait').show();
$.post('/update-password', {'oldpassword': $('#oldpassword').val(), 'newpassword': $('#newpassword').val(), 'newpassword2': $('#newpassword2').val()}, function(data) {
$('#password-wait').hide();
if (data.error) {
$('#password-message').attr('class','error').html(data.error).show();
$('#password-submit').show();
} else {
$('#password-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function _do_vm(action) {
$.get('/'+action+'-vm', function(data) {
$('#vm-message').attr('class','info').html(data.ok).show();
});
}
function reboot_vm() {
if (confirm('Opravdu chcete restartovat VM?')) {
_do_vm('reboot');
}
return false;
}
function shutdown_vm() {
if (confirm('Opravdu chcete vypnout VM?')) {
_do_vm('shutdown');
}
return false;
}

2
static/js/jquery-3.3.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
static/js/script.js Normal file
View File

@ -0,0 +1,7 @@
$(function() {
$('#menu-button').on('click', toggle_menu);
});
function toggle_menu() {
$('#menu').toggle();
}

15
templates/404.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<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">
<title>Chyba 404</title>
</head>
<body>
<h1>Stránka nebyla nalezena</h1>
<p>Stránka, kterou se pokoušíte zobrazit, nebyla na serveru nalezena. Zkontrolujte prosím URL v adresním řádku nebo se vraťte <a href="/">zpět na úvodní stránku</a>.</p>
</body>
</html>

15
templates/502.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<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">
<title>Chyba 502</title>
</head>
<body>
<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>
</body>
</html>

42
templates/layout.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<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">
<title>{% block title %}{% endblock %}</title>
<link rel="icon" href="static/img/Cluster_Spotter.png" type="image/png">
<link rel="stylesheet" href="static/css/style.css" type="text/css" media="screen">
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/script.js"></script>
{% if session.admin %}
<script src="static/js/admin.js"></script>
{% endif %}
</head>
<body>
<nav>
<div id="menu-button">
<div></div>
<div></div>
<div></div>
</div>
<ul id="menu">
<li><a href="/">Portál</a></li>
{% if session.admin %}
<li><a href="/setup-host">Nastavení hostitele</a></li>
<li><a href="/setup-apps">Nastavení aplikací</a></li>
<li><a href="/logout">Odhlášení</a></li>
{% else %}
<li><a href="/login">Přihlášení</a></li>
{% endif %}
</ul>
</nav>
<header>
<h1>CLUSTER NGO</h1>
<p>Sada softwarových nástrojů určená pro krizový management.</p>
</header>
{% block body %}{% endblock %}
</body>
</html>

26
templates/login.html Normal file
View File

@ -0,0 +1,26 @@
{% extends 'layout.html' %}
{% block title %}Přihlášení{% endblock %}
{% block body %}
<div class="setup-box">
<h2>Přihlášení</h2>
<form action="/login" method="post">
<table>
<tr>
<td>Jméno:</td>
<td>admin</td>
</tr>
<tr>
<td>Heslo</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td><input type="hidden" name="redirect" value="{{ redirect }}"></td>
<td><input type="submit" value="Přihlásit"></td>
</tr>
</table>
{% if message is defined %}
<p class="error">{{ message }}</p>
{% endif %}
</form>
</div>
{% endblock %}

352
templates/portal-admin.html Normal file
View File

@ -0,0 +1,352 @@
{% extends 'layout.html' %}
{% block title %}Cluster NGO{% endblock %}
{% block body %}
{% if is_app_visible('sahana') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('sahana-demo') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('sambro') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/EDEN.png" alt="SAMBRO Mobile" title="SAMBRO Mobile">SAMBRO Mobile</a></h2>
<p>Mobilní klient k aplikaci Sahana EDEN.<br>
<a href="https://itunes.apple.com/us/app/sambro-mobile/id1127251669"><img src="static/img/icons/iOS.png" class="ico" alt="IOS">IOS 6.0 a vyšší</a><br>
<a href="https://apkpure.com/sambro-mobile/io.sahana.sambro.mobile"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 4.0 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://sambro.{{ host }}/eden/</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('crisiscleanup') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('ckan') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('opendatakit-build') %}
{% set app = conf['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>
<p><a href="https://opendatakit.org/xiframe/">XLSForm</a> - online konverter XLS.<br>
<a href="https://opendatakit.org/downloads/download-info/odk-formuploader/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Form Uploader">ODK Form Uploader</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-validate-2/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Validate">ODK Validate</a></p>
</div>
{% endif %}
{% if is_app_visible('opendatakit') %}
{% set app = conf['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>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-briefcase/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Briefcase">ODK Briefcase</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://odk.{{ host }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="https://odk.{{ host }}/"><img src="static/img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Aggregate</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
<a href="http://geoodk.com">GeoODK Collect</a> - náhrada papírových dotazníků smartphonem.
</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('openmapkit') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/GeoODK_Collect.png" alt="GeoODK Collect" title="GeoODK Collect">GeoODK Collect</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=com.geoodk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="GeoODK Collect">GeoODK Collect pro Android</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{{ host }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://play.google.com/store/apps/details?id=org.redcross.openmapkit"><img src="static/img/icons/Android.png" class="ico" alt="Android">OpenMapKit pro Android 4.1 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('frontlinesms') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/FrontlineSync.png" alt="FrontlineSync" title="FrontlineSync">FrontlineSync</a></h2>
<p>Mobilní aplikace pro<br>
<a href="https://play.google.com/store/apps/details?id=com.simlab.frontlinesync"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 2.3 a vyšší</a>
</p>
</div>
{% endif %}
{% if is_app_visible('seeddms') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('pandora') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('ushahidi') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/Ushahidi_mobile.png" alt="Ushahidi" title="Ushahidi">Ushahidi Mobile</a></h2>
<p>Mobilní aplikace Ushahidi pro<br>
<a href="https://itunes.apple.com/us/app/ushahidi-mobile/id1205994516?mt=8"><img src="static/img/icons/iOS.png" class="ico" alt="IOS">IOS 9.0 a vyšší</a><br>
<a href="https://play.google.com/store/apps/details?id=com.ushahidi.mobile"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 4.4 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">ush.{{ host }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/SMS_Sync.png" alt="SMS Sync Gateway" title="SMS Sync Gateway">SMS Sync Gateway</a></h2>
<p>Mobilní aplikace pro<br>
<a href="https://play.google.com/store/apps/details?id=org.addhen.smssync"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 2.3 a vyšší</a>
</p>
</div>
{% endif %}
{% if is_app_visible('kanboard') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard Mobile</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=eu.it_quality.kanboard"><img src="static/img/icons/Android.png" class="ico" alt="KanBoard">KanBoard client pro Android 4.1 a vyšší</a><br>
<a href="https://f-droid.org/packages/in.andres.kandroid/"><img src="static/img/icons/Android.png" class="ico" alt="Android">Kandroid pro Android 4.2 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://kb.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('cts') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('gnuhealth') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
<li><strong>Heslo k demu:</strong> <span class="demopassword">gnusolidario</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health klienti</a></h2>
<p>Klientské aplikace platformy Tryton GNU Health pro<br>
<a href="https://downloads.tryton.org/4.2/tryton-last.exe"><img src="static/img/icons/Windows.png" class="ico" alt="Windows">Windows</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.dmg"><img src="static/img/icons/MacOS.png" class="ico" alt="MacOS">MacOS</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.tar.gz"><img src="static/img/icons/Linux.png" class="ico" alt="Linux">Linux</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">gh.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('sigmah') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('motech') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% if is_app_visible('mifosx') %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/MifosX_Mobile.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
<p>Mobilní aplikace<br>
<a href="https://github.com/openMF/android-client/releases"><img src="static/img/icons/Android.png" class="ico" alt="Mifos X">Mifos X client pro Android 3.0 a vyšší</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">mifosx.{{ host }}</span></li>
<li><strong>Tenant ID:</strong> <span>default</span></li>
</ul>
</div>
{% endif %}
{% if false %}
{% set app = conf['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>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="https://openid.net"><img src="static/img/OpenID.png" alt="OpenID" title="OpenID">OpenID</a></h2>
<p>Pro ověření identity budete potřebovat účet OpenID. Zaregistrujte se. Registraci využijete v software Sahana EDEN.</p>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/POSM.png" alt="POSM" title="POSM">POSM</a></h2>
<p><strong>Portable Open Street Map</strong> - softwarový balík na offline používání OpenStreet Map v samostatné virtuální image.</p>
</div>
{% endif %}
<div class="portal-box">
<h2><a href="http://spotter.ngo"><img src="static/img/Cluster_Spotter.png" alt="Cluster Spotter" title="Cluster Spotter">Cluster Spotter</a></h2>
<p>Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.<br>
<small>CC 4.0 CZ by <a href="http://trendspotter.cz">TS</a>. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders.</small>
</p>
</div>
{% endblock %}

122
templates/portal-user.html Normal file
View File

@ -0,0 +1,122 @@
{% extends 'layout.html' %}
{% block title %}Cluster NGO{% endblock %}
{% block body %}
{% if is_app_visible('sahana-demo') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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') %}
<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 %}
<div class="portal-box">
<h2><a href="http://spotter.ngo"><img src="static/img/Cluster_Spotter.png" alt="Cluster Spotter" title="Cluster Spotter">Cluster Spotter</a></h2>
<p>Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.<br>
<small>CC 4.0 CZ by <a href="http://trendspotter.cz">TS</a>. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders.</small>
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% 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 %}

119
templates/setup-apps.html Normal file
View File

@ -0,0 +1,119 @@
{% extends 'layout.html' %}
{% block title %}Nastavení aplikací{% endblock %}
{% block body %}
<div class="setup-box">
<h2>Správce aplikací</h2>
<p>Vyberte které aplikace mají být nainstalovány, které se mají zobrazovat na hlavní straně portálu a které mají být automaticky spuštěny při startu virtuálního stroje.</p>
<table id="app-manager">
<thead>
<tr>
<td style="width:190px">Aplikace</td>
<td style="width:70px">Zobrazena</td>
<td style="width:70px">Autostart</td>
<td style="width:190px">Stav</td>
<td>Akce</td>
</tr>
</thead>
<tbody>
{% for app in all_apps %}
{% 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>
</table>
{% if not online_packages %}
<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 %}
<p><strong>Přístupové údaje k distribučnímu serveru:</strong></p>
<form id="update-repo" action="/update-repo" method="post">
<table>
<tr>
<td>URL serveru:</td>
<td><input type="text" name="repourl" value="{{ conf['repo']['url'] }}"></td>
</tr>
<tr>
<td>Uživatelské jméno:</td>
<td><input type="text" name="repousername" value="{{ conf['repo']['user'] }}"></td>
</tr>
<tr>
<td>Heslo:</td>
<td><input type="password" name="repopassword"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="repo-submit" value="Nastavit hodnoty">
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box">
<h2>Nastavení aplikací</h2>
<p>Společné nastavení sdílené některými aplikacemi.</p>
<form id="update-common" action="/update-common" method="post">
<table>
<tr>
<td>E-mail</td>
<td><input type="text" name="email" id="email" value="{{ conf['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" id="gmaps-api-key" value="{{ conf['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>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="common-submit" value="Nastavit hodnoty">
<div id="common-message"></div>
<div id="common-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box">
<h2>Správce virtuálního stroje</h2>
<p>Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.</p>
<form id="update-password" action="/update-password" method="post">
<table>
<tr>
<td>Stávající heslo:</td>
<td><input type="password" name="oldpassword" id="oldpassword"></td>
</tr>
<tr>
<td>Nové heslo:</td>
<td><input type="password" name="newpassword" id="newpassword"></td>
</tr>
<tr>
<td>Kontrola nového hesla:</td>
<td><input type="password" name="newpassword2" id="newpassword2"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="password-submit" value="Změnit heslo">
<div id="password-message"></div>
<div id="password-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna hesla, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
<p>Restartování nebo vypnutí virtuálního stroje.</p>
<input type="button" id="reboot-vm" value="Restartovat VM">
<input type="button" id="shutdown-vm" value="Vypnout VM">
<div id="vm-message"></div>
</div>
{% endblock %}

118
templates/setup-host.html Normal file
View File

@ -0,0 +1,118 @@
{% extends 'layout.html' %}
{% block title %}Nastavení hostitele{% endblock %}
{% block body %}
<div class="setup-box">
<h2>HTTPS Hostitel</h2>
<p>Základní doménové jméno a HTTPS port na kterých budou přístupny všechny aplikace.</p>
<form id="update-host" action="/update-host" method="post">
<table>
<tr>
<td>Doména</td>
<td><input type="text" name="domain" id="domain" value="{{ conf['host']['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 class="remark">HTTPS port na kterém budou dostupné aplikace. Výchozí HTTPS port je 443.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="host-submit" value="Nastavit hostitele">
<div id="host-message"></div>
<div id="host-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box">
<h2>DNS záznamy</h2>
<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>
</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>
{% endfor %}
</ul>
<input type="button" id="verify-dns" value="Ověřit nastavení DNS">
<div id="dns-message"></div>
<div id="dns-wait" class="loader-wrap">
<div class="loader"></div>
<span>Ověřuje se nastavení DNS, prosím čekejte...</span>
</div>
</div>
<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 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'] }}">
<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>
</div>
<input type="button" id="verify-http" value="Ověřit nastavení portu 80">
<div id="http-message"></div>
<div id="http-wait" class="loader-wrap">
<div class="loader"></div>
<span>Ověřuje se nastavení firewallu a NAT pro port 80, prosím čekejte...</span>
</div>
</div>
<div class="setup-box">
<h2>HTTPS certifikát</h2>
<p>Stávající certifikát je vystaven na jméno <strong>{{ cert_info['subject'] }}</strong> vystavitelem <strong>{{ cert_info['issuer'] }}</strong> a jeho platnost vyprší <strong>{{ cert_info['expires'] }}</strong>.</p>
<form id="update-cert" action="/update-cert" method="post">
<table>
<tr>
<td>Způsob správy</td>
<td>
<select name="method" id="cert-method">
<option value="selfsigned"{% if cert_info['method'] == 'selfsigned' %} selected{% endif %}>Self-signed</option>
<option value="automatic"{% if cert_info['method'] == 'automatic' %} selected{% endif %}>Automaticky</option>
<option value="manual"{% if cert_info['method'] == 'manual' %} selected{% endif %}>Ručně</option>
</select>
</td>
<td class="remark">Volba "Self-signed" vygeneruje certifikát s vlastním podpisem a platností 20 let. Tento certifikát je použitelný pro testovací účely, ale většina mobilních aplikací s ním odmítne fungovat.
<br>Volba "Automaticky" způsobí, že systém automaticky zažádá o certifikát certifikační autority Let's Encrypt pro všechny plně kvalifikované doménové názvy (tj. nikoliv wildcard) zmíněné v sekci <em>DNS záznamy</em>. Počet žádostí o certifikát se stejným doménovým jménem je omezený na 5 týdně, proto je vhodné tento typ certifikátu nastavovat až po instalaci aplikací. Zároveň bude nainstalována úloha pro automatickou obnovu. Proces vyžádání tohoto typu certifikátu může trvat několik minut.
<br>Volba "Ručně" znamená, že soubory certifikátu a jeho soukromého klíče je nutno nahrát a následně obnovovat ručně skrze formulář na této stránce.</td>
</tr>
<tr class="cert-upload"{% if cert_info['method'] != 'manual' %} style="display:none"{% endif %}>
<td>Soubor certifikátu</td>
<td><input type="file" name="public" accept=".cer, .crt, .pem"></td>
<td class="remark">Soubor s certifikátem ve formátu PEM.<br>Pokud je podepsán certifikační autoritou třetí strany, pak by tento soubor měl mimo koncového certifikátu obsahovat i podpisový certifikát.</td>
</tr>
<tr class="cert-upload"{% if cert_info['method'] != 'manual' %} style="display:none"{% endif %}>
<td>Soubor klíče</td>
<td><input type="file" name="private" accept=".key, .pem"></td>
<td class="remark">Soubor se soukromým klíčem ve formátu PEM pro výše vybraný certifikát.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="cert-submit" value="Nastavit certifikát">
<div id="cert-message"></div>
<div id="cert-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
{% endblock %}

16
wsgi.py Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('/srv/vm')
from mgr.wsgiapp import WSGIApp
application = WSGIApp()
if __name__ == '__main__':
import os
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.serving import run_simple
run_simple('127.0.0.1', 8080, ProxyFix(application), threaded=True)