diff --git a/alpine.sh b/alpine.sh index 826b01f..3035415 100755 --- a/alpine.sh +++ b/alpine.sh @@ -85,11 +85,11 @@ chroot /mnt setup-timezone -z Europe/Prague # Install basic system apk --no-cache add apache2-utils gettext wget https://dl.dasm.cz/basic.tar -O - | tar xf - -C /mnt -chroot /mnt apk --no-cache add ca-certificates curl bridge e2fsprogs-extra gettext iptables kbd-misc libcap libressl libseccomp postfix python3 py3-bcrypt py3-cffi py3-cryptography py3-dnspython py3-jinja2 py3-requests py3-six py3-werkzeug nginx util-linux acme-sh@vm lxc@vm +chroot /mnt apk --no-cache add ca-certificates curl bridge e2fsprogs-extra gettext iptables kbd-misc libressl postfix nginx util-linux acme-sh@vm lxc@vm vmmgr@vm for SERVICE in cgroups consolefont crond iptables networking nginx ntpd postfix swap urandom vmmgr; do ln -s /etc/init.d/${SERVICE} /mnt/etc/runlevels/boot done -ADMINPWD=$(htpasswd -bnBC 10 "" "${ENCPWD}" | tr -d ':\n' | sed 's/$2y/$2b/') envsubst /mnt/srv/vm/config.json +ADMINPWD=$(htpasswd -bnBC 10 "" "${ENCPWD}" | tr -d ':\n' | sed 's/$2y/$2b/') envsubst /etc/vmmgr/config.json # Change root password echo "root:$(head -c 18 /dev/urandom | base64)" | chroot /mnt chpasswd diff --git a/app-lxc/APKBUILD b/app-lxc/APKBUILD index 5c6a32b..d2bf84f 100644 --- a/app-lxc/APKBUILD +++ b/app-lxc/APKBUILD @@ -7,7 +7,6 @@ pkgdesc="Userspace interface for the Linux kernel containment features" url="https://linuxcontainers.org/lxc/" arch="x86_64" license="GPL" -depends="gzip" options="suid !check" makedepends="automake autoconf bsd-compat-headers libcap-dev libseccomp-dev libtool linux-headers" _commit=b8ab4849432cd64d789a757e385d1d324d88a61d diff --git a/basic/etc/init.d/vmmgr b/basic/etc/init.d/vmmgr index 366d19f..f87ba86 100755 --- a/basic/etc/init.d/vmmgr +++ b/basic/etc/init.d/vmmgr @@ -1,6 +1,6 @@ #!/sbin/openrc-run -command=/srv/vm/wsgi.py +command=/usr/share/vmmgr/wsgi.py description="VM manager" pidfile=/var/run/vmmgr.pid start_stop_daemon_args="--background --make-pidfile --stderr /dev/null --stdout /dev/null" diff --git a/basic/etc/nginx/conf.d/default.conf b/basic/etc/nginx/conf.d/default.conf index 3499490..b4a6271 100644 --- a/basic/etc/nginx/conf.d/default.conf +++ b/basic/etc/nginx/conf.d/default.conf @@ -23,12 +23,12 @@ server { } location /static { - root /srv/vm; + root /usr/share/vmmgr; } error_page 502 /502.html; location = /502.html { - root /srv/vm/templates; + root /usr/share/vmmgr/templates; } location = /vm-ping { diff --git a/basic/srv/vm/config.default.json b/basic/srv/vm/config.default.json deleted file mode 100644 index 2c11b70..0000000 --- a/basic/srv/vm/config.default.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "apps": {}, - "common": { - "email": "admin@example.com", - "gmaps-api-key": "" - }, - "host": { - "adminpwd": "${ADMINPWD}", - "domain": "spotter.vm", - "firstrun": true, - "port": "443" - }, - "packages": {}, - "repo": { - "pwd": "", - "url": "https://dl.dasm.cz/spotter-repo", - "user": "" - } -} diff --git a/basic/srv/vm/mgr/__init__.py b/basic/srv/vm/mgr/__init__.py deleted file mode 100644 index 36326f5..0000000 --- a/basic/srv/vm/mgr/__init__.py +++ /dev/null @@ -1,364 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import shutil -import subprocess - -from . import tools -from . import validator -from .config import Config - -VERSION = '0.0.1' - -ISSUE_FILE = '/etc/issue' -NGINX_DIR = '/etc/nginx/conf.d' -ACME_CRON = '/etc/periodic/daily/acme-sh' -CERT_PUB_FILE = '/etc/ssl/services.pem' -CERT_KEY_FILE = '/etc/ssl/services.key' -CERT_SAN_FILE = '/etc/ssl/san.cnf' - -NGINX_TEMPLATE = '''server {{ - listen [::]:{port} ssl http2; - server_name {host}.{domain}; - - access_log /var/log/nginx/{app}.access.log; - error_log /var/log/nginx/{app}.error.log; - - location / {{ - proxy_pass http://{app}:8080; - }} - - error_page 502 /502.html; - location = /502.html {{ - root /srv/vm/templates; - }} - - location = /vm-ping {{ - add_header Content-Type text/plain; - return 200 "vm-pong"; - }} -}} -''' - -NGINX_DEFAULT_TEMPLATE = '''server {{ - listen [::]:80 default_server ipv6only=off; - - location / {{ - return 301 https://$host:{port}$request_uri; - }} - - location /.well-known/acme-challenge/ {{ - root /etc/acme.sh.d; - }} - - location = /vm-ping {{ - add_header Content-Type text/plain; - return 200 "vm-pong"; - }} -}} - -server {{ - listen [::]:{port} ssl http2 default_server ipv6only=off; - - location / {{ - proxy_pass http://127.0.0.1:8080; - }} - - location /static {{ - root /srv/vm; - }} - - error_page 502 /502.html; - location = /502.html {{ - root /srv/vm/templates; - }} - - location = /vm-ping {{ - add_header Content-Type text/plain; - return 200 "vm-pong"; - }} -}} -''' - -ISSUE_TEMPLATE = ''' -\x1b[1;32m _____ _ _ __ ____ __ - / ____| | | | | \\\\ \\\\ / / \\\\/ | - | (___ _ __ ___ | |_| |_ ___ _ _\\\\ \\\\ / /| \\\\ / | - \\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| | - ____) | |_) | (_) | |_| || __/ | \\\\ / | | | | - |_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_| \\\\/ |_| |_| - | | - |_|\x1b[0m - - \x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán. - Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení. - Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít - za následek občanské nebo trestní sankce. - - \x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited. - You must have explicit, authorized permission to access or configure this - device. Unauthorized attempts and actions to access or use this system may - result in civil or criminal penalties. - - - Pro přístup k aplikacím otevřete URL \x1b[1m{url}\x1b[0m ve Vašem - internetovém prohlížeči. - -\x1b[0;30m -''' - -ACME_CRON_TEMPLATE = '''#!/bin/sh - -[ -x /usr/bin/acme.sh ] && /usr/bin/acme.sh --cron >/dev/null -''' - -CERT_SAN = '''[ req ] -distinguished_name = dn -x509_extensions = ext -[ dn ] -[ ext ] -subjectAltName=DNS:{domain},DNS:*.{domain}" -''' - -class VMMgr: - def __init__(self): - # Load JSON configuration - self.conf = Config() - self.domain = self.conf['host']['domain'] - self.port = self.conf['host']['port'] - - def update_login(self, app, login, password): - # Update login and password for an app in the configuration - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - if login is not None: - self.conf['apps'][app]['login'] = login - if password is not None: - self.conf['apps'][app]['password'] = password - self.conf.save() - - def show_tiles(self, app): - # Update visibility for the app in the configuration - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - self.conf['apps'][app]['visible'] = True - self.conf.save() - - def hide_tiles(self, app): - # Update visibility for the app in the configuration - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - self.conf['apps'][app]['visible'] = False - self.conf.save() - - def start_app(self, app): - # Start the actual app service - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - tools.start_service(app) - - def stop_app(self, app): - # Stop the actual app service - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - tools.stop_service(app) - # Stop the app service's dependencies if they are not used by another running app - deps = self.build_deps_tree() - for dep in self.get_app_deps(app): - if not any([tools.is_service_started(d) for d in deps[dep]]): - tools.stop_service(dep) - - def build_deps_tree(self): - # Fisrt, build a dictionary of {app: [needs]} - needs = {} - for app in self.conf['apps']: - needs[app] = self.get_app_deps(app) - # Then reverse it to {need: [apps]} - deps = {} - for app, need in needs.items(): - for n in need: - deps.setdefault(n, []).append(app) - return deps - - def get_app_deps(self, app): - # Get "needs" line from init script and split it to list - try: - with open(os.path.join('/etc/init.d', app), 'r') as f: - for line in f.readlines(): - if line.strip().startswith('need'): - return line.split()[1:] - except: - pass - return [] - - def enable_autostart(self, app): - # Add the app to OpenRC default runlevel - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - subprocess.run(['/sbin/rc-update', 'add', app]) - - def disable_autostart(self, app): - # Remove the app from OpenRC default runlevel - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - subprocess.run(['/sbin/rc-update', 'del', app]) - - def prepare_container(self): - # Extract the variables from values given via lxc.hook.pre-start hook - app = os.environ['LXC_NAME'] - # Remove ephemeral layer data - tools.clean_ephemeral_layer(app) - # Configure host and common params used in the app - self.configure_app(app) - - def register_container(self): - # Extract the variables from values given via lxc.hook.start-host hook - app = os.environ['LXC_NAME'] - pid = os.environ['LXC_PID'] - # Lease the first unused IP to the container - ip = tools.get_unused_ip() - tools.update_hosts_lease(ip, app) - tools.set_container_ip(pid, ip) - - def unregister_container(self): - # Extract the variables from values given via lxc.hook.post-stop hook - app = os.environ['LXC_NAME'] - # Release the container IP - tools.update_hosts_lease(None, app) - # Remove ephemeral layer data - tools.clean_ephemeral_layer(app) - - def configure_app(self, app): - script = os.path.join('/srv', app, 'update-conf.sh') - if os.path.exists(script): - setup_env = os.environ.copy() - setup_env['DOMAIN'] = self.domain - setup_env['PORT'] = self.port - setup_env['EMAIL'] = self.conf['common']['email'] - setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key'] - subprocess.run([script], env=setup_env, check=True) - - def register_proxy(self, app): - # Setup proxy configuration and reload nginx - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f: - f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], domain=self.domain, port=self.port)) - tools.reload_nginx() - - def unregister_proxy(self, app): - # Remove proxy configuration and reload nginx - if app not in self.conf['apps']: - raise validator.InvalidValueException('app', app) - os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app))) - tools.reload_nginx() - - def update_host(self, domain, port): - # Update domain and port and rebuild all configuration. Web interface calls tools.restart_nginx() in WSGI close handler - if not validator.is_valid_domain(domain): - raise validator.InvalidValueException('domain', domain) - if not validator.is_valid_port(port): - raise validator.InvalidValueException('port', port) - self.domain = self.conf['host']['domain'] = domain - self.port = self.conf['host']['port'] = port - self.conf.save() - # Restart all apps to trigger configuration refresh - for app in self.conf['apps']: - if tools.is_service_started(app): - tools.restart_service(app) - # Rebuild and restart nginx if it was requested. - self.rebuild_nginx() - - def rebuild_nginx(self): - # Rebuild nginx config for the portal app. Web interface calls tools.restart_nginx() in WSGI close handler - with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f: - f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port)) - - def rebuild_issue(self): - # Compile the HTTPS host displayed in terminal banner - domain = self.domain - # If the dummy host is used, take an IP address of a primary interface instead - if domain == 'spotter.vm': - domain = tools.get_local_ipv4() - if not domain: - domain = tools.get_local_ipv6() - if not domain: - domain = '127.0.0.1' - # Rebuild the terminal banner - with open(ISSUE_FILE, 'w') as f: - f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(domain, self.port))) - - def update_common(self, email, gmaps_api_key): - # Update common configuration values - if email != None: - # Update email - if not validator.is_valid_email(email): - raise validator.InvalidValueException('email', email) - self.conf['common']['email'] = email - if gmaps_api_key != None: - # Update Google Maps API key - self.conf['common']['gmaps-api-key'] = gmaps_api_key - # Save config to file - self.conf.save() - for app in self.conf['apps']: - # Restart currently running apps in order to update their config - if tools.is_service_started(app): - tools.restart_service(app) - - def update_password(self, oldpassword, newpassword): - # Update LUKS password and adminpwd for WSGI application - input = '{}\n{}'.format(oldpassword, newpassword).encode() - subprocess.run(['cryptsetup', 'luksChangeKey', '/dev/sda2'], input=input, check=True) - # Update bcrypt-hashed password in config - self.conf['host']['adminpwd'] = tools.adminpwd_hash(newpassword) - # Save config to file - self.conf.save() - - def create_selfsigned_cert(self): - # Remove acme.sh cronjob - if os.path.exists(ACME_CRON): - os.unlink(ACME_CRON) - # Create selfsigned certificate with wildcard alternative subject name - with open(os.path.join(CERT_SAN_FILE), 'w') as f: - f.write(CERT_SAN.format(domain=self.domain)) - subprocess.run(['openssl', 'req', '-config', CERT_SAN_FILE, '-x509', '-new', '-out', CERT_PUB_FILE, '-keyout', CERT_KEY_FILE, '-nodes', '-days', '7305', '-subj', '/CN={}'.format(self.domain)], check=True) - os.chmod(CERT_KEY_FILE, 0o640) - - def request_acme_cert(self): - # Remove all possible conflicting certificates requested in the past - certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')] - for cert in certs: - if cert != self.domain: - subprocess.run(['/usr/bin/acme.sh', '--remove', '-d', cert]) - # Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before - if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)): - cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain] - for app in self.conf['apps']: - cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)] - cmd += ['-w', '/etc/acme.sh.d'] - # Request the certificate - subprocess.run(cmd, check=True) - # Otherwise just try to renew - else: - # Acme.sh returns code 2 on skipped renew - try: - subprocess.run(['/usr/bin/acme.sh', '--renew', '-d', self.domain], check=True) - except subprocess.CalledProcessError as e: - if e.returncode != 2: - raise - # Install the issued certificate - subprocess.run(['/usr/bin/acme.sh', '--install-cert', '-d', self.domain, '--key-file', CERT_KEY_FILE, '--fullchain-file', CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True) - # Install acme.sh cronjob - with open(ACME_CRON, 'w') as f: - f.write(ACME_CRON_TEMPLATE) - - def install_manual_cert(self, public_file, private_file): - # Remove acme.sh cronjob - if os.path.exists(ACME_CRON): - os.unlink(ACME_CRON) - # Copy certificate files - shutil.copyfile(public_file, CERT_PUB_FILE) - shutil.copyfile(private_file, CERT_KEY_FILE) - os.chmod(CERT_KEY_FILE, 0o640) - # Reload nginx - tools.reload_nginx() diff --git a/basic/srv/vm/mgr/appmgr.py b/basic/srv/vm/mgr/appmgr.py deleted file mode 100644 index 5338097..0000000 --- a/basic/srv/vm/mgr/appmgr.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- - -import hashlib -import json -import os -import requests -import shutil -import subprocess -import time -import uuid - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.serialization import load_pem_public_key -from threading import Lock - -from . import tools - -PUB_FILE = '/srv/vm/packages.pub' -LXC_ROOT = '/var/lib/lxc' - -class ActionItem: - def __init__(self, action, app): - self.timestamp = int(time.time()) - self.action = action - self.app = app - self.started = False - self.finished = False - self.data = None - -class InstallItem: - def __init__(self, total): - # Stage 0 = download, 1 = deps install, 2 = app install - self.stage = 0 - self.total = total - self.downloaded = 0 - - def __str__(self): - # Limit the disaplyed percentage between 1 - 99 for aestethical and psychological reasons - return str(max(1, min(99, round(self.downloaded / self.total * 100)))) - -class AppMgr: - def __init__(self, vmmgr): - self.vmmgr = vmmgr - self.conf = vmmgr.conf - self.online_packages = {} - self.action_queue = {} - self.lock = Lock() - - def get_repo_resource(self, url, stream=False): - return requests.get('{}/{}'.format(self.conf['repo']['url'], url), auth=(self.conf['repo']['user'], self.conf['repo']['pwd']), stream=stream) - - def fetch_online_packages(self): - # Fetches and verifies online packages. Can raise InvalidSignature - online_packages = {} - packages = self.get_repo_resource('packages').content - packages_sig = self.get_repo_resource('packages.sig').content - with open(PUB_FILE, 'rb') as f: - pub_key = load_pem_public_key(f.read(), default_backend()) - pub_key.verify(packages_sig, packages, ec.ECDSA(hashes.SHA512())) - online_packages = json.loads(packages) - # Minimze the time when self.online_packages is out of sync - self.online_packages = online_packages - - def enqueue_action(self, action, app): - # Remove actions older than 1 day - for id,item in self.action_queue.items(): - if item.timestamp < time.time() - 86400: - del self.item[id] - # Enqueue action - id = '{}:{}'.format(app, uuid.uuid4()) - item = ActionItem(action, app) - self.action_queue[id] = item - return id,item - - def get_actions(self, ids): - # Return list of requested actions - result = {} - for id in ids: - result[id] = self.action_queue[id] if id in self.action_queue else None - return result - - def process_action(self, id): - # Main method for deferred queue processing called by WSGI close handler - item = self.action_queue[id] - with self.lock: - item.started = True - try: - # Call the action method inside exclusive lock - getattr(self, item.action)(item) - except BaseException as e: - item.data = e - finally: - item.finished = True - - def start_app(self, item): - if not tools.is_service_started(item.app): - self.vmmgr.start_app(item.app) - - def stop_app(self, item): - if tools.is_service_started(item.app): - self.vmmgr.stop_app(item.app) - - def install_app(self, item): - # Main installation function. Wrapper for download, registration and install script - deps = [d for d in self.get_install_deps(item.app) if d not in self.conf['packages']] - item.data = InstallItem(sum(self.online_packages[d]['size'] for d in deps)) - for dep in deps: - self.download_package(dep, item.data) - for dep in deps: - item.data.stage = 2 if dep == deps[-1] else 1 - # Purge old data before unpacking to clean previous failed installation - self.purge_package(dep) - self.unpack_package(dep) - # Run uninstall script before installation to clean previous failed installation - self.run_uninstall_script(dep) - self.register_package(dep) - self.run_install_script(dep) - - def uninstall_app(self, item): - # Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration - self.stop_app(item) - if tools.is_service_autostarted(item.app): - self.vmmgr.disable_autostart(item.app) - deps = self.get_install_deps(item.app, False)[::-1] - for dep in deps: - if dep not in self.get_uninstall_deps(): - self.run_uninstall_script(dep) - self.purge_package(dep) - self.unregister_package(dep) - - def download_package(self, name, installitem): - tmp_archive = '/tmp/{}.tar.xz'.format(name) - r = self.get_repo_resource('{}.tar.xz'.format(name), True) - with open(tmp_archive, 'wb') as f: - for chunk in r.iter_content(chunk_size=65536): - if chunk: - installitem.downloaded += f.write(chunk) - - def unpack_package(self, name): - tmp_archive = '/tmp/{}.tar.xz'.format(name) - # Verify hash - if self.online_packages[name]['sha512'] != hash_file(tmp_archive): - raise InvalidSignature(name) - # Unpack - subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True) - os.unlink(tmp_archive) - - def purge_package(self, name): - # Removes package and shared data from filesystem - lxcpath = self.conf['packages'][name]['lxcpath'] if name in self.conf['packages'] else self.online_packages[name]['lxcpath'] - lxc_dir = os.path.join(LXC_ROOT, lxcpath) - if os.path.exists(lxc_dir): - shutil.rmtree(lxc_dir) - srv_dir = os.path.join('/srv/', name) - if os.path.exists(srv_dir): - shutil.rmtree(srv_dir) - lxc_log = '/var/log/lxc/{}.log'.format(name) - if os.path.exists(lxc_log): - os.unlink(lxc_log) - - def register_package(self, name): - # Registers a package in local configuration - metadata = self.online_packages[name] - self.conf['packages'][name] = { - 'deps': metadata['deps'], - 'lxcpath': metadata['lxcpath'], - 'version': metadata['version'] - } - # If host definition is present, register the package as application - if 'host' in metadata: - self.conf['apps'][name] = { - 'title': metadata['title'], - 'host': metadata['host'], - 'login': 'N/A', - 'password': 'N/A', - 'visible': False - } - self.conf.save() - - def unregister_package(self, name): - # Removes a package from local configuration - del self.conf['packages'][name] - if name in self.conf['apps']: - del self.conf['apps'][name] - self.conf.save() - - def run_install_script(self, name): - # Runs install.sh for a package, if the script is present - install_dir = os.path.join('/srv/', name, 'install') - install_script = os.path.join('/srv/', name, 'install.sh') - if os.path.exists(install_script): - subprocess.run(install_script, check=True) - os.unlink(install_script) - if os.path.exists(install_dir): - shutil.rmtree(install_dir) - - def run_uninstall_script(self, name): - # Runs uninstall.sh for a package, if the script is present - uninstall_script = os.path.join('/srv/', name, 'uninstall.sh') - if os.path.exists(uninstall_script): - subprocess.run(uninstall_script, check=True) - - def get_install_deps(self, name, online=True): - # Flatten dependency tree for a package while preserving the dependency order - packages = self.online_packages if online else self.conf['packages'] - deps = packages[name]['deps'].copy() - for dep in deps[::-1]: - deps[:0] = [d for d in self.get_install_deps(dep, online)] - deps = list(dict.fromkeys(deps + [name])) - return deps - - def get_uninstall_deps(self): - # Create reverse dependency tree for all installed packages - deps = {} - for pkg in self.conf['packages']: - for d in self.conf['packages'][pkg]['deps']: - deps.setdefault(d, []).append(pkg) - return deps - -def hash_file(file_path): - sha512 = hashlib.sha512() - with open(file_path, 'rb') as f: - while True: - data = f.read(65536) - if not data: - break - sha512.update(data) - return sha512.hexdigest() diff --git a/basic/srv/vm/mgr/config.py b/basic/srv/vm/mgr/config.py deleted file mode 100644 index 1bf0752..0000000 --- a/basic/srv/vm/mgr/config.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -from threading import Lock - -CONF_FILE = '/srv/vm/config.json' - -class Config: - def __init__(self): - self.lock = Lock() - self.load() - - def load(self): - with self.lock: - with open(CONF_FILE, 'r') as f: - self.data = json.load(f) - - def save(self): - with self.lock: - with open(CONF_FILE, 'w') as f: - json.dump(self.data, f, sort_keys=True, indent=4) - - def __getitem__(self, attr): - return self.data[attr] diff --git a/basic/srv/vm/mgr/tools.py b/basic/srv/vm/mgr/tools.py deleted file mode 100644 index 1c0e4fd..0000000 --- a/basic/srv/vm/mgr/tools.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- - -import bcrypt -import dns.exception -import dns.resolver -import os -import requests -import shutil -import socket -import subprocess - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.x509.oid import NameOID - -def compile_url(domain, port, proto='https'): - port = '' if (proto == 'https' and port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port) - return '{}://{}{}'.format(proto, domain, port) - -def get_local_ipv4(): - # Return first routable IPv4 address of the VM (container host) - try: - return subprocess.run(['/sbin/ip', 'route', 'get', '1'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-1] - except: - return None - -def get_local_ipv6(): - # Return first routable IPv6 address of the VM (container host) - try: - return subprocess.run(['/sbin/ip', 'route', 'get', '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-3] - except: - return None - -def get_external_ip(family): - # Return external IP address of given family via 3rd party service - allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family - try: - requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family - return requests.get('https://tools.dasm.cz/myip.php', timeout=5).text - except: - return None - finally: - requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family - -def get_external_ipv4(): - # Return external IPv4 address - return get_external_ip(socket.AF_INET) - -def get_external_ipv6(): - # Return external IPv6 address - return get_external_ip(socket.AF_INET6) - -resolver = dns.resolver.Resolver() -resolver.timeout = 3 -resolver.lifetime = 3 -resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844'] - -def resolve_ip(domain, type): - # Resolve domain name using Google Public DNS - try: - return resolver.query(domain, type)[0].address - except dns.exception.Timeout: - raise - except: - return None - -def ping_url(url): - try: - return requests.get('https://tools.dasm.cz/vm-ping.php', params = {'url': url}, timeout=5).text == 'vm-pong' - except requests.exceptions.Timeout: - raise - except: - return False - -def is_service_started(app): - # Check OpenRC service status without calling any binary - return os.path.exists(os.path.join('/run/openrc/started', app)) - -def is_service_autostarted(app): - # Check OpenRC service enablement - return os.path.exists(os.path.join('/etc/runlevels/default', app)) - -def start_service(service): - subprocess.run(['/sbin/service', service, 'start'], check=True) - -def stop_service(service): - subprocess.run(['/sbin/service', service, 'stop'], check=True) - -def restart_service(service): - subprocess.run(['/sbin/service', service, 'restart']) - -def reload_nginx(): - subprocess.run(['/usr/sbin/nginx', '-s', 'reload']) - -def restart_nginx(): - restart_service('nginx') - -def get_cert_info(cert): - # Gather certificate data important for setup-host - with open(cert, 'rb') as f: - cert = x509.load_pem_x509_certificate(f.read(), default_backend()) - data = {'subject': cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, - 'issuer': cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, - 'expires': '{} UTC'.format(cert.not_valid_after), - 'method': 'manual'} - if os.path.exists('/etc/periodic/daily/acme-sh'): - data['method'] = 'letsencrypt' - # This is really naive method of inferring if the cert is selfsigned and should never be used in production :) - elif data['subject'] == data['issuer']: - data['method'] = 'selfsigned' - return data - -def adminpwd_hash(password): - return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() - -def adminpwd_verify(password, hash): - return bcrypt.checkpw(password.encode(), hash.encode()) - -def shutdown_vm(): - subprocess.run(['/sbin/poweroff']) - -def reboot_vm(): - subprocess.run(['/sbin/reboot']) - -def get_unused_ip(): - # This is a poor man's DHCP server which uses /etc/hosts as lease database - # Leases the first unused IP from range 172.17.0.0/16 - leased = [] - with open('/etc/hosts', 'r') as f: - for line in f.read().splitlines(): - if line.startswith('172.17'): - ip = line.split()[0].split('.') - leased.append(int(ip[2]) * 256 + int(ip[3])) - for i in range(1, 65534): - if i not in leased: - break - return '172.17.{}.{}'. format(i // 256, i % 256) - -def update_hosts_lease(ip, app): - hosts = [] - with open('/etc/hosts', 'r') as f: - for line in f: - if not line.strip().endswith(' {}'.format(app)): - hosts.append(line) - if ip: - hosts.append('{} {}\n'.format(ip, app)) - with open('/etc/hosts', 'w') as f: - f.writelines(hosts) - -def set_container_ip(pid, ip): - # Set IP in container based on PID given via lxc.hook.start-host hook - cmd = 'ip addr add {}/16 broadcast 172.17.255.255 dev eth0 && ip route add default via 172.17.0.1'.format(ip) - subprocess.run(['nsenter', '-a', '-t', pid, '--', '/bin/sh', '-c', cmd]) - -def clean_ephemeral_layer(app): - layer = os.path.join('/var/lib/lxc', app, 'delta0') - if os.path.exists(layer): - for item in os.scandir(layer): - shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path) diff --git a/basic/srv/vm/mgr/validator.py b/basic/srv/vm/mgr/validator.py deleted file mode 100644 index 14571d4..0000000 --- a/basic/srv/vm/mgr/validator.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -domain_re = re.compile(r'^(?!-)[a-z0-9-]{1,63}(? 0 and port < 65536 - except: - return False - -def is_valid_email(email): - parts = email.split('@') - if len(parts) != 2: - return False - return bool(box_re.match(parts[0])) and bool(domain_re.match(parts[1])) diff --git a/basic/srv/vm/mgr/wsgiapp.py b/basic/srv/vm/mgr/wsgiapp.py deleted file mode 100644 index f39fca1..0000000 --- a/basic/srv/vm/mgr/wsgiapp.py +++ /dev/null @@ -1,399 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import os - -from werkzeug.exceptions import BadRequest, HTTPException, NotFound -from werkzeug.routing import Map, Rule -from werkzeug.utils import redirect -from werkzeug.wrappers import Request, Response -from werkzeug.wsgi import ClosingIterator -from jinja2 import Environment, FileSystemLoader - -from . import VMMgr, CERT_PUB_FILE -from . import tools -from .appmgr import AppMgr -from .validator import InvalidValueException -from .wsgilang import WSGILang -from .wsgisession import WSGISession - -SESSION_KEY = os.urandom(26) - -class WSGIApp(object): - def __init__(self): - self.vmmgr = VMMgr() - self.appmgr = AppMgr(self.vmmgr) - self.conf = self.vmmgr.conf - self.jinja_env = Environment(loader=FileSystemLoader('/srv/vm/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True) - self.jinja_env.globals.update(is_app_visible=self.is_app_visible) - self.jinja_env.globals.update(is_service_autostarted=tools.is_service_autostarted) - self.jinja_env.globals.update(is_service_started=tools.is_service_started) - - def __call__(self, environ, start_response): - return self.wsgi_app(environ, start_response) - - def wsgi_app(self, environ, start_response): - request = Request(environ) - # Reload config in case it has changed between requests - self.conf.load() - # Enhance request - request.session = WSGISession(request.cookies, SESSION_KEY) - request.session.lang = WSGILang() - # Dispatch request - response = self.dispatch_request(request) - # Save session if changed - request.session.save(response) - return response(environ, start_response) - - def dispatch_request(self, request): - adapter = self.get_url_map(request.session).bind_to_environ(request.environ) - try: - endpoint, values = adapter.match() - return getattr(self, endpoint)(request, **values) - except NotFound as e: - # Return custom 404 page - response = self.render_template('404.html', request) - response.status_code = 404 - return response - except HTTPException as e: - return e - - def get_url_map(self, session): - rules = [ - Rule('/', endpoint='portal_view'), - Rule('/login', methods=['GET'], endpoint='login_view', defaults={'redirect': '/'}), - Rule('/login', methods=['POST'], endpoint='login_action'), - Rule('/logout', endpoint='logout_action') - ] - if session['admin']: - rules += [ - Rule('/setup-host', endpoint='setup_host_view'), - Rule('/setup-apps', endpoint='setup_apps_view'), - Rule('/update-host', endpoint='update_host_action'), - Rule('/verify-dns', endpoint='verify_dns_action'), - Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}), - Rule('/verify-http', endpoint='verify_http_action', defaults={'proto': 'http'}), - Rule('/update-cert', endpoint='update_cert_action'), - Rule('/update-common', endpoint='update_common_action'), - Rule('/update-repo', endpoint='update_repo_action'), - Rule('/update-app-visibility', endpoint='update_app_visibility_action'), - Rule('/update-app-autostart', endpoint='update_app_autostart_action'), - Rule('/start-app', endpoint='start_app_action'), - Rule('/stop-app', endpoint='stop_app_action'), - Rule('/install-app', endpoint='install_app_action'), - Rule('/get-progress', endpoint='get_progress_action'), - Rule('/uninstall-app', endpoint='uninstall_app_action'), - Rule('/update-password', endpoint='update_password_action'), - Rule('/shutdown-vm', endpoint='shutdown_vm_action'), - Rule('/reboot-vm', endpoint='reboot_vm_action'), - ] - else: - rules += [ - Rule('/setup-host', endpoint='login_view', defaults={'redirect': '/setup-host'}), - Rule('/setup-apps', endpoint='login_view', defaults={'redirect': '/setup-apps'}), - ] - return Map(rules) - - def render_template(self, template_name, request, **context): - # Enhance context - context['conf'] = self.conf - context['session'] = request.session - # Render template - t = self.jinja_env.get_template(template_name) - return Response(t.render(context), mimetype='text/html') - - def render_json(self, data): - return Response(json.dumps(data), mimetype='application/json') - - def login_view(self, request, **kwargs): - return self.render_template('login.html', request, redirect=kwargs['redirect']) - - def login_action(self, request): - password = request.form['password'] - redir_url = request.form['redirect'] - if tools.adminpwd_verify(password, self.conf['host']['adminpwd']): - request.session['admin'] = True - return redirect(redir_url) - else: - return self.render_template('login.html', request, message=request.session.lang.bad_password()) - - def logout_action(self, request): - request.session.reset() - return redirect('/') - - def portal_view(self, request): - # Default portal view. If this is the first run, perform first-run setup. - if self.conf['host']['firstrun']: - # Set user as admin - request.session['admin'] = True - # Disable and save first-run flag - self.conf['host']['firstrun'] = False - self.conf.save() - # Redirect to host setup view - return redirect('/setup-host') - host = tools.compile_url(self.conf['host']['domain'], self.conf['host']['port'])[8:] - if request.session['admin']: - return self.render_template('portal-admin.html', request, host=host) - return self.render_template('portal-user.html', request, host=host) - - def setup_host_view(self, request): - # Host setup view. - ex_ipv4 = tools.get_external_ipv4() - ex_ipv6 = tools.get_external_ipv6() - in_ipv4 = tools.get_local_ipv4() - in_ipv6 = tools.get_local_ipv6() - cert_info = tools.get_cert_info(CERT_PUB_FILE) - return self.render_template('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info) - - def setup_apps_view(self, request): - # Application manager view. - try: - self.appmgr.fetch_online_packages() - except: - pass - all_apps = sorted(set([k for k,v in self.appmgr.online_packages.items() if 'host' in v] + list(self.conf['apps'].keys()))) - return self.render_template('setup-apps.html', request, all_apps=all_apps, online_packages=self.appmgr.online_packages) - - def render_setup_apps_row(self, request, app, app_title, item): - lang = request.session.lang - actions = '
' - if item.action == 'start_app': - if not item.started: - status = 'Spouští se (ve frontě)' - elif not item.finished: - status = 'Spouští se' - elif isinstance(item.data, BaseException): - status = '{}'.format(lang.stop_start_error()) - else: - status = 'Spuštěna' - actions = 'Zastavit' - elif item.action == 'stop_app': - if not item.started: - status = 'Zastavuje se (ve frontě)' - elif not item.finished: - status = 'Zastavuje se' - elif isinstance(item.data, BaseException): - status = '{}'.format(lang.stop_start_error()) - else: - status = 'Zastavena' - actions = 'Spustit, Odinstalovat' - elif item.action == 'install_app': - if not item.started: - status = 'Stahuje se (ve frontě)' - elif not item.finished: - if item.data.stage == 0: - status = 'Stahuje se ({} %)'.format(item.data) - elif item.data.stage == 1: - status = 'Instalují se závislosti' - else: - status = 'Instaluje se' - elif isinstance(item.data, BaseException): - status = '{}'.format(lang.package_manager_error()) - else: - status = 'Zastavena' - actions = 'Spustit, Odinstalovat' - elif item.action == 'uninstall_app': - if not item.started: - status = 'Odinstalovává se (ve frontě)' - elif not item.finished: - status = 'Odinstalovává se' - elif isinstance(item.data, BaseException): - status = '{}'.format(lang.package_manager_error()) - else: - status = 'Není nainstalována' - actions = 'Instalovat' - is_error = isinstance(item.data, BaseException) - t = self.jinja_env.get_template('setup-apps-row.html') - return t.render({'conf': self.conf, 'session': request.session, 'app': app, 'app_title': app_title, 'status': status, 'actions': actions, 'is_error': is_error}) - - def update_host_action(self, request): - # Update domain and port, then restart nginx - try: - domain = request.form['domain'] - port = request.form['port'] - self.vmmgr.update_host(domain, port) - server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME'] - url = '{}/setup-host'.format(tools.compile_url(server_name, port)) - response = self.render_json({'ok': request.session.lang.host_updated(url, url)}) - response.call_on_close(tools.restart_nginx) - return response - except BadRequest: - return self.render_json({'error': request.session.lang.malformed_request()}) - except InvalidValueException as e: - if e.args[0] == 'domain': - return self.render_json({'error': request.session.lang.invalid_domain(domain)}) - if e.args[0] == 'port': - return self.render_json({'error': request.session.lang.invalid_port(port)}) - - def verify_dns_action(self, request): - # Check if all FQDNs for all applications are resolvable and point to current external IP - domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']] - ipv4 = tools.get_external_ipv4() - ipv6 = tools.get_external_ipv6() - for domain in domains: - try: - a = tools.resolve_ip(domain, 'A') - aaaa = tools.resolve_ip(domain, 'AAAA') - if not a and not aaaa: - return self.render_json({'error': request.session.lang.dns_record_does_not_exist(domain)}) - if a and a != ipv4: - return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, a, ipv4)}) - if aaaa and aaaa != ipv6: - return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, aaaa, ipv6)}) - except: - return self.render_json({'error': request.session.lang.dns_timeout()}) - return self.render_json({'ok': request.session.lang.dns_records_ok()}) - - def verify_http_action(self, request, **kwargs): - # Check if all applications are accessible from the internet using 3rd party ping service - proto = kwargs['proto'] - port = self.vmmgr.port if proto == 'https' else '80' - domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']] - for domain in domains: - url = tools.compile_url(domain, port, proto) - try: - if not tools.ping_url(url): - return self.render_json({'error': request.session.lang.http_host_not_reachable(url)}) - except: - return self.render_json({'error': request.session.lang.http_timeout()}) - return self.render_json({'ok': request.session.lang.http_hosts_ok(port)}) - - def update_cert_action(self, request): - # Update certificate - either request via Let's Encrypt or manually upload files - try: - if request.form['method'] not in ['selfsigned', 'automatic', 'manual']: - raise BadRequest() - if request.form['method'] == 'selfsigned': - self.vmmgr.create_selfsigned_cert() - elif request.form['method'] == 'automatic': - self.vmmgr.request_acme_cert() - else: - if not request.files['public']: - return self.render_json({'error': request.session.lang.cert_file_missing()}) - if not request.files['private']: - return self.render_json({'error': request.session.lang.key_file_missing()}) - request.files['public'].save('/tmp/public.pem') - request.files['private'].save('/tmp/private.pem') - self.vmmgr.install_manual_cert('/tmp/public.pem', '/tmp/private.pem') - os.unlink('/tmp/public.pem') - os.unlink('/tmp/private.pem') - except BadRequest: - return self.render_json({'error': request.session.lang.malformed_request()}) - except: - return self.render_json({'error': request.session.lang.cert_request_error()}) - url = tools.compile_url(self.vmmgr.domain, self.vmmgr.port) - return self.render_json({'ok': request.session.lang.cert_installed(url, url)}) - - def update_common_action(self, request): - # Update common settings shared between apps - admin e-mail address, Google Maps API key - try: - self.vmmgr.update_common(request.form['email'], request.form['gmaps-api-key']) - except BadRequest: - return self.render_json({'error': request.session.lang.malformed_request()}) - return self.render_json({'ok': request.session.lang.common_updated()}) - - def update_repo_action(self, request): - # Update repository URL and credentials - try: - self.conf['repo']['url'] = request.form['repourl'] - self.conf['repo']['user'] = request.form['repousername'] - self.conf['repo']['pwd'] = request.form['repopassword'] - self.conf.save() - except: - pass - return redirect('/setup-apps') - - def update_app_visibility_action(self, request): - # Update application visibility on portal page - try: - if request.form['value'] == 'true': - self.vmmgr.show_tiles(request.form['app']) - else: - self.vmmgr.hide_tiles(request.form['app']) - except (BadRequest, InvalidValueException): - return self.render_json({'error': request.session.lang.malformed_request()}) - return self.render_json({'ok': 'ok'}) - - def update_app_autostart_action(self, request): - # Update value determining if the app should be automatically started after VM boot - try: - if request.form['value'] == 'true': - self.vmmgr.enable_autostart(request.form['app']) - else: - self.vmmgr.disable_autostart(request.form['app']) - except (BadRequest, InvalidValueException): - return self.render_json({'error': request.session.lang.malformed_request()}) - return self.render_json({'ok': 'ok'}) - - def enqueue_action(self, request, action): - try: - app = request.form['app'] - except BadRequest: - return self.render_json({'error': request.session.lang.malformed_request()}) - app_title = self.conf['apps'][app]['title'] if app in self.conf['apps'] else self.appmgr.online_packages[app]['title'] - id,item = self.appmgr.enqueue_action(action, app) - response = self.render_json({'html': self.render_setup_apps_row(request, app, app_title, item), 'id': id}) - response.call_on_close(lambda: self.appmgr.process_action(id)) - return response - - def start_app_action(self, request): - # Queues application start along with its dependencies - return self.enqueue_action(request, 'start_app') - - def stop_app_action(self, request): - # Queues application stop along with its dependencies - return self.enqueue_action(request, 'stop_app') - - def install_app_action(self, request): - # Queues application installation - return self.enqueue_action(request, 'install_app') - - def uninstall_app_action(self, request): - # Queues application uninstallation - return self.enqueue_action(request, 'uninstall_app') - - def get_progress_action(self, request): - # Gets appmgr queue status for given ids - json = {} - try: - ids = request.form.getlist('ids[]') - except BadRequest: - return self.render_json({'error': request.session.lang.malformed_request()}) - actions = self.appmgr.get_actions(ids) - for id,item in actions.items(): - app = item.app - # In case of installation error, we need to get the name from online_packages as the app is not yet registered - app_title = self.conf['apps'][app]['title'] if app in self.conf['apps'] else self.appmgr.online_packages[app]['title'] - json[id] = {'html': self.render_setup_apps_row(request, app, app_title, item), 'last': item.finished} - return self.render_json(json) - - def update_password_action(self, request): - # Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account - try: - if request.form['newpassword'] != request.form['newpassword2']: - return self.render_json({'error': request.session.lang.password_mismatch()}) - if request.form['newpassword'] == '': - return self.render_json({'error': request.session.lang.password_empty()}) - # No need to explicitly validate old password, update_luks_password will raise exception if it's wrong - self.vmmgr.update_password(request.form['oldpassword'], request.form['newpassword']) - except: - return self.render_json({'error': request.session.lang.bad_password()}) - return self.render_json({'ok': request.session.lang.password_changed()}) - - def reboot_vm_action(self, request): - # Reboots VM - response = self.render_json({'ok': request.session.lang.reboot_initiated()}) - response.call_on_close(tools.reboot_vm) - return response - - def shutdown_vm_action(self, request): - # Shuts down VM - response = self.render_json({'ok': request.session.lang.shutdown_initiated()}) - response.call_on_close(tools.shutdown_vm) - return response - - def is_app_visible(self, app): - return app in self.conf['apps'] and self.conf['apps'][app]['visible'] and tools.is_service_started(app) - -class InvalidRecordException(Exception): - pass diff --git a/basic/srv/vm/mgr/wsgilang.py b/basic/srv/vm/mgr/wsgilang.py deleted file mode 100644 index 8785643..0000000 --- a/basic/srv/vm/mgr/wsgilang.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- - -class WSGILang: - lang = { - 'malformed_request': 'Byl zaslán chybný požadavek. Obnovte stránku a zkuste akci zopakovat.', - 'invalid_domain': 'Zadaný doménový název "{}" není platný.', - 'invalid_port': 'Zadaný port "{}" není platný.', - 'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL {} a pokračujte následujícími kroky.', - 'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.', - 'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.', - 'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.', - 'dns_records_ok': 'DNS záznamy jsou nastaveny správně.', - 'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.', - 'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.', - 'http_hosts_ok': 'Síť je nastavena správně. Všechny aplikace na portu {} jsou z internetu dostupné.', - 'cert_file_missing': 'Nebyl vybrán soubor s certifikátem.', - 'key_file_missing': 'Nebyl vybrán soubor se soukromým klíčem.', - 'cert_request_error': 'Došlo k chybě při žádosti o certifikát. Zkontrolujte, zda je virtuální stroj dostupný z internetu na portu 80.', - 'cert_installed': 'Certifikát byl úspěšně nainstalován. Přejděte na URL {} nebo restartujte webový prohlížeč pro jeho načtení.', - 'common_updated': 'Nastavení aplikací bylo úspěšně změněno.', - 'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.', - 'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.', - 'package_manager_error': 'Došlo k chybě při instalaci aplikace. Zkuste akci opakovat nebo restartuje virtuální stroj.', - 'bad_password': 'Nesprávné heslo', - 'password_mismatch': 'Zadaná hesla se neshodují', - 'password_empty': 'Nové heslo nesmí být prázdné', - 'password_changed': 'Heslo úspěšně změněno', - 'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.', - 'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.', - } - - def __getattr__(self, key): - def function(*args): - return self.lang[key].format(*args) - return function diff --git a/basic/srv/vm/mgr/wsgisession.py b/basic/srv/vm/mgr/wsgisession.py deleted file mode 100644 index 938ba06..0000000 --- a/basic/srv/vm/mgr/wsgisession.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -from werkzeug.contrib.securecookie import SecureCookie - -class WSGISession: - def __init__(self, cookies, secret_key): - self.secret_key = secret_key - data = cookies.get('session') - if data: - self.sc = SecureCookie.unserialize(data, secret_key) - else: - self.reset() - if 'admin' not in self.sc: - self.reset() - - def __getitem__(self, key): - return self.sc.__getitem__(key) - def __setitem__(self, key, value): - return self.sc.__setitem__(key, value) - def __delitem__(self, key): - return self.sc.__delitem__(key) - def __contains__(self, key): - return self.sc.__contains__(key) - - def reset(self): - self.sc = SecureCookie(secret_key=self.secret_key) - self.sc['admin'] = False - - def save(self, response): - if self.sc.should_save: - data = self.sc.serialize() - response.set_cookie('session', data, httponly=True) diff --git a/basic/srv/vm/packages.pub b/basic/srv/vm/packages.pub deleted file mode 100644 index 60532d9..0000000 --- a/basic/srv/vm/packages.pub +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWJXH4Qm0kt2L86sntQH+C1zOJNQ0qMRt -0vx4krTxRs9HQTQYAy//JC92ea2aKleA8OL0JF90b1NYXcQCWdAS+vE/ng9IEAii -8C2+5nfuFeZ5YUjbQhfFblwHSM0c7hEG ------END PUBLIC KEY----- diff --git a/basic/srv/vm/static/css/style.css b/basic/srv/vm/static/css/style.css deleted file mode 100644 index 7ec9407..0000000 --- a/basic/srv/vm/static/css/style.css +++ /dev/null @@ -1,200 +0,0 @@ -body { - font-family: 'Calibri', 'Verdana', 'Tahoma', sans-serif; - background-color: #bbb; - color: #000; - line-height: 150%; - margin: 25px 30px; -} - -a { - color: #06f; - text-decoration: none; -} - -img { - border: 0px; -} - -nav { - float: right; -} - -nav #menu-button { - float: right; - cursor: pointer; -} - -nav #menu-button div { - width: 24px; - height: 4px; - background-color: #fff; - border: 1px solid #000; - margin: 2px 0px; -} - -nav ul { - display: none; - list-style: none; - border: 1px solid #000; - margin: 26px 0px 0px 0px; - position: absolute; - background-color: #fff; - padding: 10px; - right: 30px; - z-index: 1; -} - -nav a { - display: block; -} - -h1, h2 { - font-size: 150%; -} - -header { - color: #fff; -} - -header h1, -header p, -.portal-box p { - padding: 0px; - margin: 0px; -} - -.portal-box, -.setup-box { - background-color: #fff; - margin-top: 13px; - border: solid 1px #000; - padding: 10px; -} - -.portal-box { - position: relative; - margin-right: 13px; - width: 365px; - float: left; - height: 175px; -} - -.portal-box h2 { - margin: 0px; - font-weight: normal; -} - -.portal-box h2 a { - color: inherit; -} - -.portal-box h2 img { - float: right; - margin-left: 10px; - margin-bottom:10px; - width: 100px; - height: 100px; -} - -.portal-box ul { - margin: 0px; - padding-left: 30px; -} - -.portal-box:last-child:after { - clear: both; -} - -.portal-box-double-width { - width: 765px; -} - -.ico { - margin-right: 5px; - width: 20px; - height: 20px; - vertical-align: top; -} - -.setup-box h2 { - margin-top: 0px; -} - -.setup-box input[type="text"], -.setup-box input[type="password"], -.setup-box input[type="submit"], -.setup-box input[type="button"], -.setup-box input[type="file"], -.setup-box select { - box-sizing: border-box; - width: 180px; -} - -.setup-box table { - border-collapse: collapse; -} - -.setup-box thead { - font-weight: bold; -} - -.setup-box td { - padding: 1px 10px; - vertical-align: top; -} - -.setup-box td:first-child { - white-space: nowrap; -} - -.setup-box td.remark { - color: #999; - font-size: 80%; - font-style: italic; - line-height: 125%; - padding-top: 5px; -} - -#app-manager { - table-layout: fixed; -} - -.center { - text-align: center; -} - -.error { - color: #c00; - font-weight: bold; -} - -.info { - color: #090; - font-weight: bold; -} - -.loader-wrap { - display: none; -} - -.loader-wrap span:after { - clear: both; - content: ''; - display: table; -} - -.loader { - float: left; - width: 14px; - height: 14px; - border: 5px solid #eee; - border-top: 5px solid #fa3; - border-radius: 50%; - margin-right: 5px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} diff --git a/basic/srv/vm/static/img/CAP.png b/basic/srv/vm/static/img/CAP.png deleted file mode 100644 index e8eb9f4..0000000 Binary files a/basic/srv/vm/static/img/CAP.png and /dev/null differ diff --git a/basic/srv/vm/static/img/CKAN.png b/basic/srv/vm/static/img/CKAN.png deleted file mode 100644 index 77cfb7e..0000000 Binary files a/basic/srv/vm/static/img/CKAN.png and /dev/null differ diff --git a/basic/srv/vm/static/img/CTS.png b/basic/srv/vm/static/img/CTS.png deleted file mode 100644 index 9bd35f4..0000000 Binary files a/basic/srv/vm/static/img/CTS.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Cluster_Spotter.png b/basic/srv/vm/static/img/Cluster_Spotter.png deleted file mode 100644 index dc32e20..0000000 Binary files a/basic/srv/vm/static/img/Cluster_Spotter.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Crisis_Cleanup.png b/basic/srv/vm/static/img/Crisis_Cleanup.png deleted file mode 100644 index 6979185..0000000 Binary files a/basic/srv/vm/static/img/Crisis_Cleanup.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Diaspora.png b/basic/srv/vm/static/img/Diaspora.png deleted file mode 100644 index ec88dca..0000000 Binary files a/basic/srv/vm/static/img/Diaspora.png and /dev/null differ diff --git a/basic/srv/vm/static/img/EDEN.png b/basic/srv/vm/static/img/EDEN.png deleted file mode 100644 index 6fd72a1..0000000 Binary files a/basic/srv/vm/static/img/EDEN.png and /dev/null differ diff --git a/basic/srv/vm/static/img/FrontlineSMS.png b/basic/srv/vm/static/img/FrontlineSMS.png deleted file mode 100644 index de2b91b..0000000 Binary files a/basic/srv/vm/static/img/FrontlineSMS.png and /dev/null differ diff --git a/basic/srv/vm/static/img/FrontlineSync.png b/basic/srv/vm/static/img/FrontlineSync.png deleted file mode 100644 index 9c6ad76..0000000 Binary files a/basic/srv/vm/static/img/FrontlineSync.png and /dev/null differ diff --git a/basic/srv/vm/static/img/GNU_Health.png b/basic/srv/vm/static/img/GNU_Health.png deleted file mode 100644 index 779ca5a..0000000 Binary files a/basic/srv/vm/static/img/GNU_Health.png and /dev/null differ diff --git a/basic/srv/vm/static/img/GeoODK_Collect.png b/basic/srv/vm/static/img/GeoODK_Collect.png deleted file mode 100644 index 8f0831d..0000000 Binary files a/basic/srv/vm/static/img/GeoODK_Collect.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Kanboard.png b/basic/srv/vm/static/img/Kanboard.png deleted file mode 100644 index 969bad6..0000000 Binary files a/basic/srv/vm/static/img/Kanboard.png and /dev/null differ diff --git a/basic/srv/vm/static/img/MifosX.png b/basic/srv/vm/static/img/MifosX.png deleted file mode 100644 index 29ceb11..0000000 Binary files a/basic/srv/vm/static/img/MifosX.png and /dev/null differ diff --git a/basic/srv/vm/static/img/MifosX_Mobile.png b/basic/srv/vm/static/img/MifosX_Mobile.png deleted file mode 100644 index 5c06ab9..0000000 Binary files a/basic/srv/vm/static/img/MifosX_Mobile.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Motech.png b/basic/srv/vm/static/img/Motech.png deleted file mode 100644 index 55636a8..0000000 Binary files a/basic/srv/vm/static/img/Motech.png and /dev/null differ diff --git a/basic/srv/vm/static/img/ODK.png b/basic/srv/vm/static/img/ODK.png deleted file mode 100644 index 3d48ef5..0000000 Binary files a/basic/srv/vm/static/img/ODK.png and /dev/null differ diff --git a/basic/srv/vm/static/img/ODK_Collect.png b/basic/srv/vm/static/img/ODK_Collect.png deleted file mode 100644 index 4862290..0000000 Binary files a/basic/srv/vm/static/img/ODK_Collect.png and /dev/null differ diff --git a/basic/srv/vm/static/img/OMK.png b/basic/srv/vm/static/img/OMK.png deleted file mode 100644 index fdaa652..0000000 Binary files a/basic/srv/vm/static/img/OMK.png and /dev/null differ diff --git a/basic/srv/vm/static/img/OpenID.png b/basic/srv/vm/static/img/OpenID.png deleted file mode 100644 index 8263f07..0000000 Binary files a/basic/srv/vm/static/img/OpenID.png and /dev/null differ diff --git a/basic/srv/vm/static/img/POSM.png b/basic/srv/vm/static/img/POSM.png deleted file mode 100644 index 0ed788e..0000000 Binary files a/basic/srv/vm/static/img/POSM.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Pandora.png b/basic/srv/vm/static/img/Pandora.png deleted file mode 100644 index 3eb5771..0000000 Binary files a/basic/srv/vm/static/img/Pandora.png and /dev/null differ diff --git a/basic/srv/vm/static/img/PostGIS.png b/basic/srv/vm/static/img/PostGIS.png deleted file mode 100644 index 6e01af3..0000000 Binary files a/basic/srv/vm/static/img/PostGIS.png and /dev/null differ diff --git a/basic/srv/vm/static/img/SMS_Sync.png b/basic/srv/vm/static/img/SMS_Sync.png deleted file mode 100644 index 8084f12..0000000 Binary files a/basic/srv/vm/static/img/SMS_Sync.png and /dev/null differ diff --git a/basic/srv/vm/static/img/SeedDMS.png b/basic/srv/vm/static/img/SeedDMS.png deleted file mode 100644 index aec78d5..0000000 Binary files a/basic/srv/vm/static/img/SeedDMS.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Sigmah.png b/basic/srv/vm/static/img/Sigmah.png deleted file mode 100644 index 8b07dbe..0000000 Binary files a/basic/srv/vm/static/img/Sigmah.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Ushahidi.png b/basic/srv/vm/static/img/Ushahidi.png deleted file mode 100644 index e568cbe..0000000 Binary files a/basic/srv/vm/static/img/Ushahidi.png and /dev/null differ diff --git a/basic/srv/vm/static/img/Ushahidi_mobile.png b/basic/srv/vm/static/img/Ushahidi_mobile.png deleted file mode 100644 index 6a29201..0000000 Binary files a/basic/srv/vm/static/img/Ushahidi_mobile.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/Android.png b/basic/srv/vm/static/img/icons/Android.png deleted file mode 100644 index f974b23..0000000 Binary files a/basic/srv/vm/static/img/icons/Android.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/Java.png b/basic/srv/vm/static/img/icons/Java.png deleted file mode 100644 index 196c11e..0000000 Binary files a/basic/srv/vm/static/img/icons/Java.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/Linux.png b/basic/srv/vm/static/img/icons/Linux.png deleted file mode 100644 index 5a79ac9..0000000 Binary files a/basic/srv/vm/static/img/icons/Linux.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/MacOS.png b/basic/srv/vm/static/img/icons/MacOS.png deleted file mode 100644 index 5bfe192..0000000 Binary files a/basic/srv/vm/static/img/icons/MacOS.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/Windows.png b/basic/srv/vm/static/img/icons/Windows.png deleted file mode 100644 index 0fc7e3f..0000000 Binary files a/basic/srv/vm/static/img/icons/Windows.png and /dev/null differ diff --git a/basic/srv/vm/static/img/icons/iOS.png b/basic/srv/vm/static/img/icons/iOS.png deleted file mode 100644 index 69f5429..0000000 Binary files a/basic/srv/vm/static/img/icons/iOS.png and /dev/null differ diff --git a/basic/srv/vm/static/js/admin.js b/basic/srv/vm/static/js/admin.js deleted file mode 100644 index 1aaa527..0000000 --- a/basic/srv/vm/static/js/admin.js +++ /dev/null @@ -1,229 +0,0 @@ -var action_queue = []; - -$(function() { - $('#update-host').on('submit', update_host); - $('#verify-dns').on('click', verify_dns); - $('#verify-https').on('click', verify_https); - $('#verify-http').on('click', verify_http); - $('#cert-method').on('change', toggle_cert_method); - $('#update-cert').on('submit', update_cert); - $('#update-common').on('submit', update_common); - $('#app-manager') - .on('click', '.app-visible', update_app_visibility) - .on('click', '.app-autostart', update_app_autostart) - .on('click', '.app-start', start_app) - .on('click', '.app-stop', stop_app) - .on('click', '.app-install', install_app) - .on('click', '.app-uninstall', uninstall_app); - $('#update-password').on('submit', update_password); - $('#reboot-vm').on('click', reboot_vm); - $('#shutdown-vm').on('click', shutdown_vm); - window.setInterval(check_progress, 1000); -}); - -function update_host() { - $('#host-submit').hide(); - $('#host-message').hide(); - $('#host-wait').show(); - $.post('/update-host', {'domain': $('#domain').val(), 'port': $('#port').val()}, function(data) { - $('#host-wait').hide(); - if (data.error) { - $('#host-message').attr('class','error').html(data.error).show(); - $('#host-submit').show(); - } else { - $('#host-message').attr('class','info').html(data.ok).show(); - $('input').prop('disabled', true); - $('.setup-box').slice(1).css('opacity', '0.5'); - } - }); - return false; -} - -function verify_dns() { - $('#verify-dns').hide(); - $('#dns-message').hide(); - $('#dns-wait').show(); - $.get('/verify-dns', function(data) { - $('#dns-wait').hide(); - if (data.error) { - $('#dns-message').attr('class','error').html(data.error).show(); - $('#verify-dns').show(); - } else { - $('#dns-message').attr('class','info').html(data.ok).show(); - } - }); - return false; -} - -function _verify_http(proto) { - $('#verify-'+proto).hide(); - $('#'+proto+'-message').hide(); - $('#'+proto+'-wait').show(); - $.get('/verify-' + proto, function(data) { - $('#'+proto+'-wait').hide(); - if (data.error) { - $('#'+proto+'-message').attr('class','error').html(data.error).show(); - $('#verify-'+proto).show(); - } else { - $('#'+proto+'-message').attr('class','info').html(data.ok).show(); - } - }); - return false; -} - -function verify_http() { - return _verify_http('http'); -} - -function verify_https() { - return _verify_http('https'); -} - -function toggle_cert_method() { - if ($('#cert-method').val() == 'manual') { - $('.cert-upload').show(); - } else { - $('.cert-upload').hide(); - } -} - -function update_cert() { - $('#cert-submit').hide(); - $('#cert-message').hide(); - $('#cert-wait').show(); - $.ajax({url: '/update-cert', type: 'POST', data: new FormData($('#update-cert')[0]), cache: false, contentType: false, processData: false, success: function(data) { - $('#cert-wait').hide(); - if (data.error) { - $('#cert-message').attr('class','error').html(data.error).show(); - $('#cert-submit').show(); - } else { - $('#cert-message').attr('class','info').html(data.ok).show(); - } - }}); - return false; -} - -function update_common() { - $('#common-submit').hide(); - $('#common-message').hide(); - $('#common-wait').show(); - $.post('/update-common', {'email': $('#email').val(), 'gmaps-api-key': $('#gmaps-api-key').val()}, function(data) { - $('#common-wait').hide(); - if (data.error) { - $('#common-message').attr('class','error').html(data.error).show(); - $('#common-submit').show(); - } else { - $('#common-message').attr('class','info').html(data.ok).show(); - $('#common-submit').show(); - } - }); - return false; -} - -function _update_app(item, ev) { - var el = $(ev.target); - var app = el.closest('tr').data('app'); - var value = el.is(':checked') ? 'true' : ''; - $.post('/update-app-'+item, {'app': app, 'value': value}, function(data) { - if (data.error) { - el.prop('checked', !value); - alert(data.error); - } - }); -} - -function update_app_visibility(ev) { - return _update_app('visibility', ev); -} - -function update_app_autostart(ev) { - return _update_app('autostart', ev); -} - -function _do_app(action, ev) { - var el = $(ev.target); - var tr = el.closest('tr'); - var td = el.closest('td'); - td.html(''); - $.post('/'+action+'-app', {'app': tr.data('app')}, function(data) { - if (data.error) { - td.attr('class','error').html(data.error); - } else if (action) { - tr.html(data.html); - action_queue.push(data.id); - } - }); - return false; -} - -function start_app(ev) { - return _do_app('start', ev); -} - -function stop_app(ev) { - return _do_app('stop', ev); -} - -function install_app(ev) { - return _do_app('install', ev); -} - -function uninstall_app(ev) { - var app = $(ev.target).closest('tr').children().first().text() - if (confirm('Opravdu chcete odinstalovat aplikaci '+app+'?')) { - return _do_app('uninstall', ev); - } - return false; -} - -function check_progress() { - if (action_queue.length) { - $.post('/get-progress', {'ids': action_queue}, function(data) { - for (id in data) { - var app = id.split(':')[0]; - $('#app-manager tr[data-app="'+app+'"]').html(data[id].html); - if (data[id].last) { - action_queue = action_queue.filter(function(item) { - return item !== id - }); - } - } - }); - } -} - -function update_password() { - $('#password-submit').hide(); - $('#password-message').hide(); - $('#password-wait').show(); - $.post('/update-password', {'oldpassword': $('#oldpassword').val(), 'newpassword': $('#newpassword').val(), 'newpassword2': $('#newpassword2').val()}, function(data) { - $('#password-wait').hide(); - if (data.error) { - $('#password-message').attr('class','error').html(data.error).show(); - $('#password-submit').show(); - } else { - $('#password-message').attr('class','info').html(data.ok).show(); - } - }); - return false; -} - -function _do_vm(action) { - $.get('/'+action+'-vm', function(data) { - $('#vm-message').attr('class','info').html(data.ok).show(); - }); -} - -function reboot_vm() { - if (confirm('Opravdu chcete restartovat VM?')) { - _do_vm('reboot'); - } - return false; -} - -function shutdown_vm() { - if (confirm('Opravdu chcete vypnout VM?')) { - _do_vm('shutdown'); - } - return false; -} diff --git a/basic/srv/vm/static/js/jquery-3.3.1.min.js b/basic/srv/vm/static/js/jquery-3.3.1.min.js deleted file mode 100644 index 4d9b3a2..0000000 --- a/basic/srv/vm/static/js/jquery-3.3.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&nSada softwarových nástrojů určená pro krizový management.
-