Add VMMgr/PkgMgr install progress
This commit is contained in:
parent
920f01cf45
commit
951ae86520
@ -11,7 +11,6 @@
|
||||
"port": "443"
|
||||
},
|
||||
"packages": {},
|
||||
"pending-packages": {},
|
||||
"repo": {
|
||||
"pwd": "",
|
||||
"url": "https://dl.dasm.cz/spotter-repo",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -19,6 +19,7 @@ class WSGILang:
|
||||
'cert_installed': 'Certifikát byl úspěšně nainstalován. Přejděte na URL <a href="{}">{}</a> nebo restartujte webový prohlížeč pro jeho načtení.',
|
||||
'common_updated': 'Nastavení aplikací bylo úspěšně změněno.',
|
||||
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
|
||||
'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.',
|
||||
'package_manager_error': 'Došlo k chybě při instalaci aplikace',
|
||||
'bad_password': 'Nesprávné heslo',
|
||||
'password_mismatch': 'Zadaná hesla se neshodují',
|
||||
|
@ -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();
|
||||
|
@ -2,6 +2,6 @@
|
||||
<td>{{ app_title }}</td>
|
||||
<td class="center"><input type="checkbox" class="app-visible"{% if app not in conf['apps'] %} disabled{% elif conf['apps'][app]['visible'] %} checked{% endif %}></td>
|
||||
<td class="center"><input type="checkbox" class="app-autostart"{% if app not in conf['apps'] %} disabled{% elif is_service_autostarted(app) %} checked{% endif %}></td>
|
||||
<td>{% if app not in conf['apps'] %} Není nainstalována{% elif is_service_started(app) %}<span class="info">Spuštěna</span>{% else %}<span class="error">Zastavena</span>{% endif %}</td>
|
||||
<td>{% if pending %}Instalace (<span id="install-progress">0</span> %){% elif app not in conf['apps'] %} Není nainstalována{% elif is_service_started(app) %}<span class="info">Spuštěna</span>{% else %}<span class="error">Zastavena</span>{% endif %}</td>
|
||||
<td>{% if app not in conf['apps'] %}<a href="#" class="app-install">Instalovat</a>{% else %}{% if is_service_started(app) %}<a href="#" class="app-stop">Zastavit</a>{% else %}<a href="#" class="app-start">Spustit</a>{% endif %}, <a href="#" class="app-uninstall">Odinstalovat</a>{% endif %}</td>
|
||||
</tr>
|
||||
|
Loading…
Reference in New Issue
Block a user