diff --git a/basic/srv/vm/config.json b/basic/srv/vm/config.json index d487a4c..5a98870 100644 --- a/basic/srv/vm/config.json +++ b/basic/srv/vm/config.json @@ -11,7 +11,6 @@ "port": "443" }, "packages": {}, - "pending-packages": {}, "repo": { "pwd": "", "url": "https://dl.dasm.cz/spotter-repo", diff --git a/basic/srv/vm/mgr/pkgmgr.py b/basic/srv/vm/mgr/pkgmgr.py index f84510c..b390847 100644 --- a/basic/srv/vm/mgr/pkgmgr.py +++ b/basic/srv/vm/mgr/pkgmgr.py @@ -14,16 +14,17 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_public_key -from .config import Config - PUB_FILE = '/srv/vm/packages.pub' LXC_ROOT = '/var/lib/lxc' class PackageManager: - def __init__(self): + def __init__(self, conf): # Load JSON configuration - self.conf = Config() + self.conf = conf self.online_packages = {} + self.pending = False + self.pending_to_download = 0 + self.pending_downloaded = 0 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) @@ -37,14 +38,22 @@ class PackageManager: pub_key.verify(packages_sig, packages, ec.ECDSA(hashes.SHA512())) self.online_packages = json.loads(packages) + def register_pending_installation(self): + # Registers pending installation. Fetch online packages here instead of install_pacakges() to fail early if the repo isn't reachable + self.fetch_online_packages() + self.pending = True + self.pending_to_download = 1 + self.pending_downloaded = 0 + def install_package(self, name): # Main installation function. Wrapper for download, registration and install script - self.fetch_online_packages() - for dep in self.get_deps(name): - if dep not in self.conf['packages']: - self.download_package(dep) - self.register_package(dep) - self.run_install_script(dep) + deps = d for d in self.get_deps(name) if d not in self.conf['packages'] + self.pending_to_download = sum(self.online_packages[d]['size'] for d in deps) + for dep in deps: + self.download_package(dep) + self.register_package(dep) + self.run_install_script(dep) + self.pending = False def uninstall_package(self, name): # Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration @@ -60,7 +69,7 @@ class PackageManager: with open(tmp_archive, 'wb') as f: for chunk in r.iter_content(chunk_size=65536): if chunk: - f.write(chunk) + self.pending_downloaded += f.write(chunk) # Verify hash if self.online_packages[name]['sha512'] != hash_file(tmp_archive): raise InvalidSignature(name) diff --git a/basic/srv/vm/mgr/wsgiapp.py b/basic/srv/vm/mgr/wsgiapp.py index 734daa8..d196c83 100644 --- a/basic/srv/vm/mgr/wsgiapp.py +++ b/basic/srv/vm/mgr/wsgiapp.py @@ -22,6 +22,8 @@ SESSION_KEY = os.urandom(26) class WSGIApp(object): def __init__(self): self.vmmgr = VMMgr() + self.conf = self.vmmgr.conf + self.pkgmgr = PackageManager(self.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) @@ -32,8 +34,8 @@ class WSGIApp(object): def wsgi_app(self, environ, start_response): request = Request(environ) - # Reload VM Manager config in case it has changed - self.vmmgr.conf.load() + # 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() @@ -78,6 +80,7 @@ class WSGIApp(object): Rule('/start-app', endpoint='start_app_action'), Rule('/stop-app', endpoint='stop_app_action'), Rule('/install-app', endpoint='install_app_action'), + Rule('/get-install-progress', endpoint='get_install_progress_action'), Rule('/uninstall-app', endpoint='uninstall_app_action'), Rule('/update-password', endpoint='update_password_action'), Rule('/shutdown-vm', endpoint='shutdown_vm_action'), @@ -87,7 +90,7 @@ class WSGIApp(object): def render_template(self, template_name, request, **context): # Enhance context - context['conf'] = self.vmmgr.conf + context['conf'] = self.conf context['session'] = request.session # Render template t = self.jinja_env.get_template(template_name) @@ -101,7 +104,7 @@ class WSGIApp(object): def login_action(self, request): password = request.form['password'] - if tools.adminpwd_verify(password, self.vmmgr.conf['host']['adminpwd']): + if tools.adminpwd_verify(password, self.conf['host']['adminpwd']): request.session['admin'] = True return redirect('/') else: @@ -113,15 +116,15 @@ class WSGIApp(object): def portal_view(self, request): # Default portal view. If this is the first run, perform first-run setup. - if self.vmmgr.conf['host']['firstrun']: + if self.conf['host']['firstrun']: # Set user as admin request.session['admin'] = True # Disable and save first-run flag - self.vmmgr.conf['host']['firstrun'] = False - self.vmmgr.conf.save() + self.conf['host']['firstrun'] = False + self.conf.save() # Redirect to host setup view return redirect('/setup-host') - host = tools.compile_url(self.vmmgr.conf['host']['domain'], self.vmmgr.conf['host']['port'], None) + host = tools.compile_url(self.conf['host']['domain'], self.conf['host']['port'], None) if request.session['admin']: return self.render_template('portal-admin.html', request, host=host) return self.render_template('portal-user.html', request, host=host) @@ -138,14 +141,13 @@ class WSGIApp(object): def setup_apps_view(self, request): # Application manager view. - pkgmgr = PackageManager() - pkgmgr.fetch_online_packages() - all_apps = sorted(set([k for k,v in pkgmgr.online_packages.items() if 'host' in v] + list(self.vmmgr.conf['apps'].keys()))) - return self.render_template('setup-apps.html', request, all_apps=all_apps, online_packages=pkgmgr.online_packages) + self.pkgmgr.fetch_online_packages() + all_apps = sorted(set([k for k,v in self.pkgmgr.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.pkgmgr.online_packages) - def render_setup_apps_row(self, app, app_title): + def render_setup_apps_row(self, app, app_title, pending=False): t = self.jinja_env.get_template('setup-apps-row.html') - return t.render({'app': app, 'app_title': app_title, 'conf': self.vmmgr.conf}) + return t.render({'app': app, 'app_title': app_title, 'conf': self.conf, 'pending': pending}) def update_host_action(self, request): # Update domain and port, then restart nginx @@ -168,7 +170,7 @@ class WSGIApp(object): 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.vmmgr.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.vmmgr.conf['apps']] + 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: @@ -189,7 +191,7 @@ class WSGIApp(object): # 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.vmmgr.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.vmmgr.conf['apps']] + 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: @@ -262,7 +264,7 @@ class WSGIApp(object): return self.render_json({'error': request.session.lang.malformed_request()}) except: return self.render_json({'error': request.session.lang.stop_start_error()}) - app_title = self.vmmgr.conf['apps'][app]['title'] + app_title = self.conf['apps'][app]['title'] return self.render_json({'ok': self.render_setup_apps_row(app, app_title)}) def stop_app_action(self, request): @@ -274,22 +276,31 @@ class WSGIApp(object): return self.render_json({'error': request.session.lang.malformed_request()}) except: return self.render_json({'error': request.session.lang.stop_start_error()}) - app_title = self.vmmgr.conf['apps'][app]['title'] + app_title = self.conf['apps'][app]['title'] return self.render_json({'ok': self.render_setup_apps_row(app, app_title)}) def install_app_action(self, request): - # Installs application + # Registers the application installation as pending. + if self.pkgmgr.pending: + return self.render_json({'error': request.session.lang.installation_in_progress()}) try: app = request.form['app'] - pkgmgr = PackageManager() - pkgmgr.install_package(app) + self.pkgmgr.register_pending_installation() except (BadRequest, InvalidValueException): return self.render_json({'error': request.session.lang.malformed_request()}) except: return self.render_json({'error': request.session.lang.package_manager_error()}) - # Reload config and get fresh data - self.vmmgr.conf.load() - app_title = self.vmmgr.conf['apps'][app]['title'] + app_title = self.pkgmgr.online_packages[app]['title'] + response = self.render_json({'ok': self.render_setup_apps_row(app, app_title, True)}) + response.call_on_close(lambda: pkgmgr.install_package(app)) + return response + + def get_install_progress_action(self, request): + if self.pkgmgr.pending: + return self.render_json({'progress': '{0:.1f}'.format(self.pkgmgr.pending_downloaded / self.pkgmgr.pending_to_download)}) + self.conf.load() + app = request.form['app'] + app_title = self.conf['apps'][app]['title'] return self.render_json({'ok': self.render_setup_apps_row(app, app_title)}) def uninstall_app_action(self, request): @@ -304,8 +315,8 @@ class WSGIApp(object): except: return self.render_json({'error': request.session.lang.package_manager_error()}) # Get title from old data, then reload config - app_title = self.vmmgr.conf['apps'][app]['title'] - self.vmmgr.conf.load() + app_title = self.conf['apps'][app]['title'] + self.conf.load() return self.render_json({'ok': self.render_setup_apps_row(app, app_title)}) def update_password_action(self, request): @@ -334,7 +345,7 @@ class WSGIApp(object): return response def is_app_visible(self, app): - return app in self.vmmgr.conf['apps'] and self.vmmgr.conf['apps'][app]['visible'] and tools.is_service_started(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 index a202fdc..353916c 100644 --- a/basic/srv/vm/mgr/wsgilang.py +++ b/basic/srv/vm/mgr/wsgilang.py @@ -19,6 +19,7 @@ class WSGILang: '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', 'bad_password': 'Nesprávné heslo', 'password_mismatch': 'Zadaná hesla se neshodují', diff --git a/basic/srv/vm/static/js/admin.js b/basic/srv/vm/static/js/admin.js index 3bf8175..78208b3 100644 --- a/basic/srv/vm/static/js/admin.js +++ b/basic/srv/vm/static/js/admin.js @@ -16,6 +16,7 @@ $(function() { $('#update-password').on('submit', update_password); $('#reboot-vm').on('click', reboot_vm); $('#shutdown-vm').on('click', shutdown_vm); + window.setTimeout(check_progress, 1000); }); function update_host() { @@ -171,6 +172,20 @@ function uninstall_app(ev) { return false; } +function check_progress() { + var progress = $('#install-progress'); + if (progress.length) { + var tr = progress.closest('tr'); + $.get('/get-install-progress', {'app': tr.data('app')}, function(data) { + if (data.progress) { + progress.text(data.progress); + } else { + tr.replaceWith(data.ok); + } + }); + } +} + function update_password() { $('#password-submit').hide(); $('#password-message').hide(); diff --git a/basic/srv/vm/templates/setup-apps-row.html b/basic/srv/vm/templates/setup-apps-row.html index 378e1de..e0a0eb0 100644 --- a/basic/srv/vm/templates/setup-apps-row.html +++ b/basic/srv/vm/templates/setup-apps-row.html @@ -2,6 +2,6 @@