Move vmmgr to separate app- directory
19
config.default.json
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
After Width: | Height: | Size: 5.4 KiB |
BIN
static/img/CKAN.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
static/img/CTS.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/Cluster_Spotter.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
static/img/Crisis_Cleanup.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
static/img/Diaspora.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/EDEN.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/img/FrontlineSMS.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
static/img/FrontlineSync.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
static/img/GNU_Health.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
static/img/GeoODK_Collect.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
static/img/Kanboard.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/MifosX.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
static/img/MifosX_Mobile.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
static/img/Motech.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
static/img/ODK.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/ODK_Collect.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
static/img/OMK.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
static/img/OpenID.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
static/img/POSM.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
static/img/Pandora.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/PostGIS.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/SMS_Sync.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/img/SeedDMS.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/Sigmah.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
static/img/Ushahidi.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
static/img/Ushahidi_mobile.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
static/img/icons/Android.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/icons/Java.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/icons/Linux.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
static/img/icons/MacOS.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/icons/Windows.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
static/img/icons/iOS.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
229
static/js/admin.js
Normal 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
7
static/js/script.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
$(function() {
|
||||||
|
$('#menu-button').on('click', toggle_menu);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggle_menu() {
|
||||||
|
$('#menu').toggle();
|
||||||
|
}
|
15
templates/404.html
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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 %}
|
23
templates/setup-apps-row.html
Normal 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
@ -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> </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> </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> </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
@ -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> </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> </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
@ -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)
|