Fix bugs and missing pieces, add SPOC config reload
This commit is contained in:
parent
31372ac3e1
commit
be054ed17b
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user