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

View File

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
import configparser
import importlib
import os
import shutil
import subprocess
import urllib
from spoc import config as spoc_config, repo_local, repo_online
from spoc.app import App
from spoc.config import ONLINE_BASE_URL
from spoc.container import Container, ContainerState
from spoc.depsolver import DepSolver
from spoc.image import Image
from . import config, crypto, net, templates
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):
# 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):
# Check OpenRC service enablement
@ -169,7 +171,7 @@ def uninstall_app(app_name, queue):
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]
retained_containers = [definition for name,definition in repo_local.get_containers().items() if name not in removed_containers]
remove_unused_layers(retained_containers, queue)
def update_app(app_name, queue):
@ -186,7 +188,7 @@ def update_app(app_name, queue):
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
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)
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):
# 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)
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))
spoc_config['repo']['url'] = ONLINE_BASE_URL = url
with open('/etc/spoc/spoc.conf', 'w') as f:
config.write(f)
repo_config = configparser.ConfigParser()
repo_config.read(spoc_config.CONFIG_FILE)
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():
# 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
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 spoc import repo_online, repo_local
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.wrappers import Request, Response
@ -161,8 +161,8 @@ class WSGIApp:
# Application manager view.
repo_error = None
try:
# Populate online_repo cache or fail early when the repo can't be reached
repo_online.get_apps()
# Repopulate online_repo cache or fail early when the repo can't be reached
repo_online.load(True)
except InvalidSignature:
repo_error = request.session.lang.invalid_packages_signature()
except Unauthorized:
@ -177,7 +177,11 @@ class WSGIApp:
def render_setup_apps_table(self, request):
lang = request.session.lang
local_apps = repo_local.get_apps()
try:
online_apps = repo_online.get_apps()
except:
online_apps = {}
apps_config = config.get_apps()
actionable_apps = sorted(set(online_apps) | set(local_apps))
pending_actions = self.queue.get_actions()
app_data = {}
@ -185,12 +189,12 @@ class WSGIApp:
installed = app in local_apps
title = local_apps[app]['meta']['title'] if installed else online_apps[app]['meta']['title']
try:
visible = local_apps[app]['visible']
except:
visible = apps_config[app]['visible']
except KeyError:
visible = False
try:
autostarted = local_apps[app]['autostart']
except:
except KeyError:
autostarted = False
if app in pending_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):
# 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.units_total:
# 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>'
else:
# Display queued (pending, not started) task
@ -260,8 +265,11 @@ class WSGIApp:
else:
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>'
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>'
except KeyError:
pass
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)
@ -369,7 +377,7 @@ class WSGIApp:
app = request.form['app']
self.queue.enqueue_action(app, action)
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
def start_app_action(self, request):

View File

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