Fix bugs and missing pieces, add SPOC config reload

This commit is contained in:
Disassembler 2020-04-03 21:06:54 +02:00
parent 31372ac3e1
commit be054ed17b
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
4 changed files with 56 additions and 43 deletions

View File

@ -3,7 +3,7 @@
from enum import Enum from enum import Enum
from collections import deque from collections import deque
from threading import Lock from threading import Lock
from spoc.config import LOCK_FILE from spoc import config as spoc_config
from spoc.flock import locked from spoc.flock import locked
class ActionItemType(Enum): class ActionItemType(Enum):
@ -19,20 +19,15 @@ class ActionItemType(Enum):
APP_STOP = 10 APP_STOP = 10
class ActionItem: class ActionItem:
def __init__(self, action_type, key, action, show_progress=True): def __init__(self, action_type, key, action):
self.type = action_type self.type = action_type
self.key = key self.key = key
self.action = action self.action = action
self.show_progress = show_progress self.units_total = 0
self.units_total = 1
self.units_done = 0 self.units_done = 0
def run(self): def run(self):
if self.show_progress:
self.action(self) self.action(self)
else:
self.action()
self.units_done = 1
class ActionAppQueue: class ActionAppQueue:
def __init__(self, action): def __init__(self, action):
@ -46,28 +41,28 @@ class ActionAppQueue:
self.queue.append(ActionItem(ActionItemType.IMAGE_UNPACK, image.name, image.unpack_downloaded)) self.queue.append(ActionItem(ActionItemType.IMAGE_UNPACK, image.name, image.unpack_downloaded))
def delete_image(self, image): def delete_image(self, image):
self.queue.append(ActionItem(ActionItemType.IMAGE_DELETE, image.name, image.delete, False)) self.queue.append(ActionItem(ActionItemType.IMAGE_DELETE, image.name, image.delete))
def install_app(self, app): def install_app(self, app):
self.queue.append(ActionItem(ActionItemType.APP_DOWNLOAD, app.name, app.download)) 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_UNPACK, app.name, app.unpack_downloaded))
self.queue.append(ActionItem(ActionItemType.APP_INSTALL, app.name, app.install, False)) self.queue.append(ActionItem(ActionItemType.APP_INSTALL, app.name, app.install))
def update_app(self, app): def update_app(self, app):
self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop))
self.queue.append(ActionItem(ActionItemType.APP_DOWNLOAD, app.name, app.download)) 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_UNPACK, app.name, app.unpack_downloaded))
self.queue.append(ActionItem(ActionItemType.APP_UPDATE, app.name, app.update, False)) self.queue.append(ActionItem(ActionItemType.APP_UPDATE, app.name, app.update))
def uninstall_app(self, app): def uninstall_app(self, app):
self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop))
self.queue.append(ActionItem(ActionItemType.APP_UNINSTALL, app.name, app.uninstall, False)) self.queue.append(ActionItem(ActionItemType.APP_UNINSTALL, app.name, app.uninstall))
def start_app(self, app): def start_app(self, app):
self.queue.append(ActionItem(ActionItemType.APP_START, app.name, app.start, False)) self.queue.append(ActionItem(ActionItemType.APP_START, app.name, app.start))
def stop_app(self, app): def stop_app(self, app):
self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop, False)) self.queue.append(ActionItem(ActionItemType.APP_STOP, app.name, app.stop))
def process(self): def process(self):
for item in self.queue: for item in self.queue:
@ -96,13 +91,16 @@ class ActionQueue:
self.actions[app_name] = ActionAppQueue(action) self.actions[app_name] = ActionAppQueue(action)
self.queue.append(app_name) self.queue.append(app_name)
@locked(LOCK_FILE) def process(self):
def process_actions(self):
# Main method for deferred queue processing called by WSGI close handler # Main method for deferred queue processing called by WSGI close handler
with self.lock: with self.lock:
# If the queue is being processesd by another thread, allow this thread to be terminated # If the queue is being processesd by another thread, allow this thread to be terminated
if self.is_running: if self.is_running:
return return
self.process_actions()
@locked(spoc_config.LOCK_FILE)
def process_actions(self):
while True: while True:
with self.lock: with self.lock:
# Try to get an item from queue # Try to get an item from queue

View File

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import configparser import configparser
import importlib
import os import os
import shutil import shutil
import subprocess import subprocess
import urllib import urllib
from spoc import config as spoc_config, repo_local, repo_online
from spoc.app import App from spoc.app import App
from spoc.config import ONLINE_BASE_URL
from spoc.container import Container, ContainerState from spoc.container import Container, ContainerState
from spoc.depsolver import DepSolver from spoc.depsolver import DepSolver
from spoc.image import Image
from . import config, crypto, net, templates from . import config, crypto, net, templates
from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR
@ -145,7 +147,7 @@ def update_app_autostart(app_name, enabled):
def is_app_started(app_name): def is_app_started(app_name):
# Assume that the main container has always the same name as app # Assume that the main container has always the same name as app
return Container(app_name).get_status() == ContainerState.RUNNING return Container(app_name).get_state() == ContainerState.RUNNING
def is_app_autostarted(app_name): def is_app_autostarted(app_name):
# Check OpenRC service enablement # Check OpenRC service enablement
@ -169,7 +171,7 @@ def uninstall_app(app_name, queue):
queue.uninstall_app(app) queue.uninstall_app(app)
# Remove unused layers # Remove unused layers
removed_containers = [container.name for container in app.containers] 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] retained_containers = [definition for name,definition in repo_local.get_containers().items() if name not in removed_containers]
remove_unused_layers(retained_containers, queue) remove_unused_layers(retained_containers, queue)
def update_app(app_name, queue): def update_app(app_name, queue):
@ -186,7 +188,7 @@ def update_app(app_name, queue):
queue.update_app(app) queue.update_app(app)
# Remove unused layers # Remove unused layers
removed_containers = [container.name for container in app.containers] 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 retained_containers = [definition for name,definition in repo_local.get_containers().items() if name not in removed_containers] + new_containers
remove_unused_layers(retained_containers, queue) remove_unused_layers(retained_containers, queue)
def remove_unused_layers(retained_containers, queue): def remove_unused_layers(retained_containers, queue):
@ -205,18 +207,22 @@ def remove_unused_layers(retained_containers, queue):
def update_repo_conf(url, username, password): def update_repo_conf(url, username, password):
# Include credentials in the repo URL and save to SPOC config # Include credentials in the repo URL and save to SPOC config
spoc_config = configparser.ConfigParser()
spoc_config.read('/etc/spoc/spoc.conf')
parts = urllib.parse.urlsplit(url) parts = urllib.parse.urlsplit(url)
netloc = f'{username}:{password}@{url}' if username or password else url netloc = f'{username}:{password}@{parts.netloc}' if username or password else parts.netloc
url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
spoc_config['repo']['url'] = ONLINE_BASE_URL = url repo_config = configparser.ConfigParser()
with open('/etc/spoc/spoc.conf', 'w') as f: repo_config.read(spoc_config.CONFIG_FILE)
config.write(f) repo_config['repo']['url'] = url
with open(spoc_config.CONFIG_FILE, 'w') as f:
repo_config.write(f)
# Reimport the config module, reloading the defined URL values
importlib.reload(spoc_config)
repo_online.data = None
def get_repo_conf(): def get_repo_conf():
# Parse the SPOC config repo URL and return as tuple # Parse the SPOC config repo URL and return as tuple
parts = urllib.parse.urlsplit(ONLINE_BASE_URL) parts = urllib.parse.urlsplit(spoc_config.ONLINE_BASE_URL)
netloc = parts.netloc.split('@', 1)[1] if parts.username or parts.password else parts.netloc 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)) url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
return {'url':url, 'username':parts.username} username = parts.username if parts.username else ''
return {'url':url, 'username':username}

View File

@ -8,7 +8,7 @@ from math import floor
from pkg_resources import parse_version from pkg_resources import parse_version
from spoc import repo_online, repo_local from spoc import repo_online, repo_local
from werkzeug.exceptions import HTTPException, NotFound, Unauthorized from werkzeug.exceptions import HTTPException, NotFound, Unauthorized
from werkzeug.routing import Map, Rule from werkzeug.routing import Map, Rule, RequestRedirect
from werkzeug.utils import redirect from werkzeug.utils import redirect
from werkzeug.wrappers import Request, Response from werkzeug.wrappers import Request, Response
@ -161,8 +161,8 @@ class WSGIApp:
# Application manager view. # Application manager view.
repo_error = None repo_error = None
try: try:
# Populate online_repo cache or fail early when the repo can't be reached # Repopulate online_repo cache or fail early when the repo can't be reached
repo_online.get_apps() repo_online.load(True)
except InvalidSignature: except InvalidSignature:
repo_error = request.session.lang.invalid_packages_signature() repo_error = request.session.lang.invalid_packages_signature()
except Unauthorized: except Unauthorized:
@ -177,7 +177,11 @@ class WSGIApp:
def render_setup_apps_table(self, request): def render_setup_apps_table(self, request):
lang = request.session.lang lang = request.session.lang
local_apps = repo_local.get_apps() local_apps = repo_local.get_apps()
try:
online_apps = repo_online.get_apps() online_apps = repo_online.get_apps()
except:
online_apps = {}
apps_config = config.get_apps()
actionable_apps = sorted(set(online_apps) | set(local_apps)) actionable_apps = sorted(set(online_apps) | set(local_apps))
pending_actions = self.queue.get_actions() pending_actions = self.queue.get_actions()
app_data = {} app_data = {}
@ -185,12 +189,12 @@ class WSGIApp:
installed = app in local_apps installed = app in local_apps
title = local_apps[app]['meta']['title'] if installed else online_apps[app]['meta']['title'] title = local_apps[app]['meta']['title'] if installed else online_apps[app]['meta']['title']
try: try:
visible = local_apps[app]['visible'] visible = apps_config[app]['visible']
except: except KeyError:
visible = False visible = False
try: try:
autostarted = local_apps[app]['autostart'] autostarted = local_apps[app]['autostart']
except: except KeyError:
autostarted = False autostarted = False
if app in pending_actions: if app in pending_actions:
# Display queued or currently processed actions # Display queued or currently processed actions
@ -231,8 +235,9 @@ class WSGIApp:
if app_queue.action not in (vmmgr.start_app, vmmgr.stop_app): 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 # 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}' status = f'[{app_queue.index}/{len(app_queue.queue)}] {status}'
if action_item.show_progress: if action_item.units_total:
status = f'{status} ({floor(current_action.units_done/current_action.units_total*100)} %)' # Show progress for tasks which have measurable progress
status = f'{status} ({floor(action_item.units_done/action_item.units_total*100)} %)'
actions = '<div class="loader"></div>' actions = '<div class="loader"></div>'
else: else:
# Display queued (pending, not started) task # Display queued (pending, not started) task
@ -260,8 +265,11 @@ class WSGIApp:
else: else:
status = f'<span class="error">{lang.status_stopped()}</span>' status = f'<span class="error">{lang.status_stopped()}</span>'
actions = f'<a href="#" class="app-start">{lang.action_start()}</a>, <a href="#" class="app-uninstall">{lang.action_uninstall()}</a>' actions = f'<a href="#" class="app-start">{lang.action_start()}</a>, <a href="#" class="app-uninstall">{lang.action_uninstall()}</a>'
if parse_version(online_apps[app]['version']) > parse_version(app.version): try:
if parse_version(online_apps[app]['version']) > parse_version(local_apps[app]['version']):
actions = f'{actions}, <a href="#" class="app-update">{lang.action_update()}</a>' actions = f'{actions}, <a href="#" class="app-update">{lang.action_update()}</a>'
except KeyError:
pass
app_data[app] = {'title': title, 'visible': visible, 'installed': installed, 'autostarted': autostarted, 'status': status, 'actions': actions} app_data[app] = {'title': title, 'visible': visible, 'installed': installed, 'autostarted': autostarted, 'status': status, 'actions': actions}
return self.render_template('setup-apps-table.html', request, app_data=app_data) return self.render_template('setup-apps-table.html', request, app_data=app_data)
@ -369,7 +377,7 @@ class WSGIApp:
app = request.form['app'] app = request.form['app']
self.queue.enqueue_action(app, action) self.queue.enqueue_action(app, action)
response = self.render_json({'ok': self.render_setup_apps_table(request)}) response = self.render_json({'ok': self.render_setup_apps_table(request)})
response.call_on_close(self.queue.process_actions) response.call_on_close(self.queue.process)
return response return response
def start_app_action(self, request): def start_app_action(self, request):

View File

@ -44,6 +44,7 @@ class WSGILang:
'status_installing': 'Instaluje se', 'status_installing': 'Instaluje se',
'status_updating': 'Aktualizuje se', 'status_updating': 'Aktualizuje se',
'status_uninstalling': 'Odinstalovává se', 'status_uninstalling': 'Odinstalovává se',
'status_deleting': 'Maže se {}',
'status_not_installed': 'Není nainstalována', 'status_not_installed': 'Není nainstalována',
'action_start': 'Spustit', 'action_start': 'Spustit',
'action_stop': 'Zastavit', 'action_stop': 'Zastavit',