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