diff --git a/usr/lib/python3.8/vmmgr/actionqueue.py b/usr/lib/python3.8/vmmgr/actionqueue.py index 2f75938..f3b18dd 100644 --- a/usr/lib/python3.8/vmmgr/actionqueue.py +++ b/usr/lib/python3.8/vmmgr/actionqueue.py @@ -15,6 +15,8 @@ class ActionItemType(Enum): APP_INSTALL = 6 APP_UPDATE = 7 APP_UNINSTALL = 8 + APP_START = 9 + APP_STOP = 10 class ActionItem: def __init__(self, action_type, key, action, show_progress=True): @@ -36,7 +38,6 @@ class ActionAppQueue: def __init__(self, action): self.action = action self.queue = [] - self.started = False self.exception = None self.index = 0 @@ -53,13 +54,21 @@ class ActionAppQueue: self.queue.append(ActionItem(ActionItemType.APP_INSTALL, app.name, app.install, False)) def update_app(self, app): + self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) self.queue.append(ActionItem(ActionItemType.APP_DOWNLOAD, app.name, app.download)) self.queue.append(ActionItem(ActionItemType.APP_UNPACK, app.name, app.unpack_downloaded)) self.queue.append(ActionItem(ActionItemType.APP_UPDATE, app.name, app.update, False)) def uninstall_app(self, app): + self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) self.queue.append(ActionItem(ActionItemType.APP_UNINSTALL, app.name, app.uninstall, False)) + def start_app(self, app): + self.queue.append(ActionItem(ActionItemType.APP_START, app.name, app.start, False)) + + def stop_app(self, app): + self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) + def process(self): for item in self.queue: self.index += 1 diff --git a/usr/lib/python3.8/vmmgr/config.py b/usr/lib/python3.8/vmmgr/config.py index b0132e8..d9166d9 100644 --- a/usr/lib/python3.8/vmmgr/config.py +++ b/usr/lib/python3.8/vmmgr/config.py @@ -50,13 +50,13 @@ def unregister_app(name): pass @locked(CONF_LOCK) -def set_host_value(key, value): +def set_host(key, value): load() data['host'][key] = value save() @locked(CONF_LOCK) -def set_app_value(name, key, value): +def set_app(name, key, value): load() data['apps'][name][key] = value save() diff --git a/usr/lib/python3.8/vmmgr/crypto.py b/usr/lib/python3.8/vmmgr/crypto.py index ecd8c0e..91a797d 100644 --- a/usr/lib/python3.8/vmmgr/crypto.py +++ b/usr/lib/python3.8/vmmgr/crypto.py @@ -9,12 +9,10 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID -from . import config from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE -def create_selfsigned_cert(): +def create_selfsigned_cert(domain): # Create selfsigned certificate with wildcard alternative subject name - domain = config.get_host()['domain'] private_key = ec.generate_private_key(ec.SECP384R1(), default_backend()) public_key = private_key.public_key() subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, domain)]) @@ -59,5 +57,5 @@ def get_cert_info(): def adminpwd_hash(password): return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() -def adminpwd_verify(password): - return bcrypt.checkpw(password.encode(), config.get_host()['adminpwd'].encode()) +def adminpwd_verify(password, hash): + return bcrypt.checkpw(password.encode(), hash.encode()) diff --git a/usr/lib/python3.8/vmmgr/vmmgr.py b/usr/lib/python3.8/vmmgr/vmmgr.py index fb02f1e..012c804 100644 --- a/usr/lib/python3.8/vmmgr/vmmgr.py +++ b/usr/lib/python3.8/vmmgr/vmmgr.py @@ -8,8 +8,9 @@ import urllib from spoc.app import App from spoc.config import ONLINE_BASE_URL from spoc.container import Container, ContainerState +from spoc.depsolver import DepSolver -from . import crypto, net, templates +from . import config, crypto, net, templates from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR def register_app(app, host, login, password): @@ -68,7 +69,7 @@ def update_password(oldpassword, newpassword): pwinput = f'{oldpassword}\n{newpassword}'.encode() partition_uuid = open('/etc/crypttab').read().split()[1][5:] partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip() - subprocess.run(['cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True) + subprocess.run(['/sbin/cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True) # Update bcrypt-hashed password in config hash = crypto.adminpwd_hash(newpassword) config.set_host('adminpwd', hash) @@ -126,28 +127,13 @@ def shutdown_vm(): def reboot_vm(): subprocess.run(['/sbin/reboot']) -def start_app(item): - # Start the actual app service - app = item.key - if app in config.get_apps() and not is_app_started(app): - start_service(app) +def start_app(app_name, queue): + # Enqueue application start + queue.start_app(App(app_name)) -def start_service(service): - subprocess.run(['/sbin/service', service, 'start'], check=True) - -def stop_app(item): - # Stop the actual app service - app = item.key - if app in config.get_apps() and is_app_started(app): - stop_service(app) - # Stop the app service's dependencies if they are not used by another running app - deps = get_services_deps() - for dep in get_service_deps(app): - if not any([is_app_started(d) for d in deps[dep]]): - stop_service(dep) - -def stop_service(service): - subprocess.run(['/sbin/service', service, 'stop'], check=True) +def stop_app(app_name, queue): + # Enqueue application stop + queue.stop_app(App(app_name)) def update_app_visibility(app_name, visible): # Update visibility for the app in the configuration @@ -155,7 +141,7 @@ def update_app_visibility(app_name, visible): def update_app_autostart(app_name, enabled): # Add/remove the app to OpenRC default runlevel - App(app_name).set_autostart(enabled) + App(app_name, False).set_autostart(enabled) def is_app_started(app_name): # Assume that the main container has always the same name as app @@ -166,9 +152,10 @@ def is_app_autostarted(app_name): return App(app_name, False).autostart def install_app(app_name, queue): - # Main installation function. Wrapper for download, registration and install script + # Enqueue layers and application download, registration and install script execution required_images = [] - for container in repo_online.get_app(app_name)['containers'].values(): + new_containers = repo_online.get_app(app_name)['containers'].values() + for container in new_containers: required_images.extend(repo_online.get_image(container['image'])['layers']) local_images = repo_local.get_images() for layer in set(required_images): @@ -177,21 +164,46 @@ def install_app(app_name, queue): queue.install_app(App(app_name, False, False)) def uninstall_app(app_name, queue): - # Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration - queue.uninstall_app(App(app_name, False)) + # Enqueue application uninstall script execution and removal + app = App(app_name, False) + queue.uninstall_app(app) + # Remove unused layers + removed_containers = [container.name for container in app.containers] + retained_containers = [definition for name,definition in repo_local.get_containers() if name not in removed_containers] + remove_unused_layers(retained_containers, queue) def update_app(app_name, queue): - # Main update function. Wrapper for download and update script + # Enqueue layers and application download, registration and update script execution required_images = [] - for container in repo_online.get_app(app_name)['containers'].values(): + new_containers = repo_online.get_app(app_name)['containers'].values() + for container in new_containers: required_images.extend(repo_online.get_image(container['image'])['layers']) local_images = repo_local.get_images() for layer in set(required_images): if layer not in local_images: queue.download_image(Image(layer, False)) - queue.update_app(App(app_name, False)) + app = App(app_name, False) + queue.update_app(app) + # Remove unused layers + removed_containers = [container.name for container in app.containers] + retained_containers = [definition for name,definition in repo_local.get_containers() if name not in removed_containers] + new_containers + remove_unused_layers(retained_containers, queue) -def update_repo_settings(url, username, password): +def remove_unused_layers(retained_containers, queue): + # Enqueue removal of images which won't be used in any locally defined containers anymore + used_images = set() + for definition in retained_containers: + used_images.update(definition['layers']) + # Build dependency tree to safely remove the images in order of dependency + depsolver = DepSolver() + for image in set(repo_local.get_images()) - used_images: + image = Image(image) + depsolver.add(image.name, set(image.layers) - used_images, image) + # Enqueue the removal actions + for image in reversed(depsolver.solve()): + queue.delete_image(image) + +def update_repo_conf(url, username, password): # Include credentials in the repo URL and save to SPOC config spoc_config = configparser.ConfigParser() spoc_config.read('/etc/spoc/spoc.conf') @@ -202,9 +214,9 @@ def update_repo_settings(url, username, password): with open('/etc/spoc/spoc.conf', 'w') as f: config.write(f) -def get_repo_settings(): +def get_repo_conf(): # Parse the SPOC config repo URL and return as tuple parts = urllib.parse.urlsplit(ONLINE_BASE_URL) netloc = parts.netloc.split('@', 1)[1] if parts.username or parts.password else parts.netloc url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) - return (url, parts.username, parts.password) + return {'url':url, 'username':parts.username} diff --git a/usr/lib/python3.8/vmmgr/wsgiapp.py b/usr/lib/python3.8/vmmgr/wsgiapp.py index 91cb610..610e522 100644 --- a/usr/lib/python3.8/vmmgr/wsgiapp.py +++ b/usr/lib/python3.8/vmmgr/wsgiapp.py @@ -125,7 +125,8 @@ class WSGIApp: def login_action(self, request): password = request.form['password'] redir = request.form['redir'] - if crypto.adminpwd_verify(password): + hash = config.get_host()['adminpwd'] + if crypto.adminpwd_verify(password, hash): request.session['admin'] = True return redirect(f'/{redir}') request.session['msg'] = f'login:error:{request.session.lang.bad_password()}' @@ -170,8 +171,8 @@ class WSGIApp: repo_error = request.session.lang.repo_unavailable() table = self.render_setup_apps_table(request) message = self.get_session_message(request) - repo_url, repo_user, _ = vmmgr.get_repo_settings() - return self.render_html('setup-apps.html', request, repo_url=repo_url, repo_user=repo_user, repo_error=repo_error, table=table, message=message) + repo_conf = vmmgr.get_repo_conf() + return self.render_html('setup-apps.html', request, repo_conf=repo_conf, repo_error=repo_error, table=table, message=message) def render_setup_apps_table(self, request): lang = request.session.lang @@ -210,27 +211,28 @@ class WSGIApp: actions = None else: # Display task/subtask progress - if app_queue.action == vmmgr.start_app: + action_item = app_queue.queue[app_queue.index-1] + if action_item.type in (ActionItemType.IMAGE_DOWNLOAD, ActionItemType.APP_DOWNLOAD): + status = lang.status_downloading(action_item.key) + elif action_item.type in (ActionItemType.IMAGE_UNPACK, ActionItemType.APP_UNPACK): + status = lang.status_unpacking(action_item.key) + elif action_item.type == ActionItemType.IMAGE_DELETE: + status = lang.status_deleting(action_item.key) + elif action_item.type == ActionItemType.APP_START: status = lang.status_starting() - elif app_queue.action == vmmgr.stop_app: + elif action_item.type == ActionItemType.APP_STOP: status = lang.status_stopping() - else: - action_item = app_queue.queue[app_queue.index-1] - if action_item.type in (ActionItemType.IMAGE_DOWNLOAD, ActionItemType.APP_DOWNLOAD): - status = lang.status_downloading(action_item.key) - elif action_item.type in (ActionItemType.IMAGE_UNPACK, ActionItemType.APP_UNPACK): - status = lang.status_unpacking(action_item.key) - elif action_item.type == ActionItemType.IMAGE_DELETE: - status = lang.status_deleting(action_item.key) - elif action_item.type == ActionItemType.APP_INSTALL: - status = lang.status_installing(action_item.key) - elif action_item.type == ActionItemType.APP_UPDATE: - status = lang.status_updating(action_item.key) - elif action_item.type == ActionItemType.APP_UNINSTALL: - status = lang.status_uninstalling(action_item.key) + elif action_item.type == ActionItemType.APP_INSTALL: + status = lang.status_installing() + elif action_item.type == ActionItemType.APP_UPDATE: + status = lang.status_updating() + elif action_item.type == ActionItemType.APP_UNINSTALL: + status = lang.status_uninstalling() + if app_queue.action not in (vmmgr.start_app, vmmgr.stop_app): + # For tasks other than start/stop which have only a single subtask, display also index of the subtask in queue status = f'[{app_queue.index}/{len(app_queue.queue)}] {status}' - if action_item.show_progress: - status = f'{status} ({floor(current_action.units_done/current_action.units_total*100)} %)' + if action_item.show_progress: + status = f'{status} ({floor(current_action.units_done/current_action.units_total*100)} %)' actions = '
' else: # Display queued (pending, not started) task @@ -239,11 +241,11 @@ class WSGIApp: elif app_queue.action == vmmgr.stop_app: status = lang.status_stopping() elif app_queue.action == vmmgr.install_app: - status = lang.status_installing('') - elif app_queue.action == vmmgr.uninstall_app: - status = lang.status_uninstalling('') + status = lang.status_installing() elif app_queue.action == vmmgr.update_app: - status = lang.status_updating('') + status = lang.status_updating() + elif app_queue.action == vmmgr.uninstall_app: + status = lang.status_uninstalling() status = f'{status} ({lang.status_queued()})' actions = '' else: @@ -348,7 +350,7 @@ class WSGIApp: if not validator.is_valid_repo_url(url): request.session['msg'] = f'repo:error:{request.session.lang.invalid_url(url)}' else: - vmmgr.update_repo_settings(url, request.form['repousername'], request.form['repopassword']) + vmmgr.update_repo_conf(url, request.form['repousername'], request.form['repopassword']) request.session['msg'] = f'repo:info:{request.session.lang.repo_updated()}' return redirect('/setup-apps') @@ -406,7 +408,7 @@ class WSGIApp: 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 + # No need to explicitly validate old password, vmmgr.update_password will raise exception if it's wrong vmmgr.update_password(request.form['oldpassword'], request.form['newpassword']) except: return self.render_json({'error': request.session.lang.bad_password()}) diff --git a/usr/lib/python3.8/vmmgr/wsgilang.py b/usr/lib/python3.8/vmmgr/wsgilang.py index c6f1517..8d5d26c 100644 --- a/usr/lib/python3.8/vmmgr/wsgilang.py +++ b/usr/lib/python3.8/vmmgr/wsgilang.py @@ -41,9 +41,9 @@ class WSGILang: 'status_stopped': 'Zastavena', 'status_downloading': 'Stahuje se {}', 'status_unpacking': 'Rozbaluje se {}', - 'status_installing': 'Instaluje se {}', - 'status_updating': 'Aktualizuje se {}', - 'status_uninstalling': 'Odinstalovává se {}', + 'status_installing': 'Instaluje se', + 'status_updating': 'Aktualizuje se', + 'status_uninstalling': 'Odinstalovává se', 'status_not_installed': 'Není nainstalována', 'action_start': 'Spustit', 'action_stop': 'Zastavit', diff --git a/usr/share/vmmgr/templates/setup-apps.html b/usr/share/vmmgr/templates/setup-apps.html index f0edbf4..56cccd7 100644 --- a/usr/share/vmmgr/templates/setup-apps.html +++ b/usr/share/vmmgr/templates/setup-apps.html @@ -26,11 +26,11 @@URL serveru: | -+ | |
Uživatelské jméno: | -+ | |
Heslo: |