Start with race condition mitigations

This commit is contained in:
Disassembler 2018-10-28 18:20:27 +01:00
parent f675996e60
commit 25689d6345
Signed by: Disassembler
GPG Key ID: 524BD33A0EE29499
7 changed files with 75 additions and 58 deletions

View File

@ -129,7 +129,7 @@ class VMMgr:
def update_login(self, app, login, password):
# Update login and password for an app in the configuration
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
if login is not None:
self.conf['apps'][app]['login'] = login
@ -139,27 +139,27 @@ class VMMgr:
def show_tiles(self, app):
# Update visibility for the app in the configuration
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
self.conf['apps'][app]['visible'] = True
self.conf.save()
def hide_tiles(self, app):
# Update visibility for the app in the configuration
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
self.conf['apps'][app]['visible'] = False
self.conf.save()
def start_app(self, app):
# Start the actual app service
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
tools.start_service(app)
def stop_app(self, app):
# Stop the actual app service
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
tools.stop_service(app)
# Stop the app service's dependencies if they are not used by another running app
@ -193,13 +193,13 @@ class VMMgr:
def enable_autostart(self, app):
# Add the app to OpenRC default runlevel
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
subprocess.run(['/sbin/rc-update', 'add', app])
def disable_autostart(self, app):
# Remove the app from OpenRC default runlevel
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
subprocess.run(['/sbin/rc-update', 'del', app])
@ -240,7 +240,7 @@ class VMMgr:
def register_proxy(self, app):
# Setup proxy configuration and reload nginx
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], domain=self.domain, port=self.port))
@ -248,7 +248,7 @@ class VMMgr:
def unregister_proxy(self, app):
# Remove proxy configuration and reload nginx
if not validator.is_valid_app(app, self.conf):
if app not in self.conf['apps']:
raise validator.InvalidValueException('app', app)
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
tools.reload_nginx()

View File

@ -22,7 +22,7 @@ class PackageManager:
# Load JSON configuration
self.conf = conf
self.online_packages = {}
self.pending = 0
self.bytes_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)
@ -40,7 +40,7 @@ class PackageManager:
def register_pending_installation(self, name):
# 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 = 1
self.bytes_downloaded = 1
# Return total size for download
deps = [d for d in self.get_install_deps(name) if d not in self.conf['packages']]
return sum(self.online_packages[d]['size'] for d in deps)
@ -48,11 +48,15 @@ class PackageManager:
def install_package(self, name):
# Main installation function. Wrapper for download, registration and install script
deps = [d for d in self.get_install_deps(name) if d not in self.conf['packages']]
for dep in deps:
self.download_package(dep)
self.register_package(dep)
self.run_install_script(dep)
self.pending = 0
try:
for dep in deps:
self.download_package(dep)
self.register_package(dep)
self.run_install_script(dep)
self.bytes_downloaded = 0
except:
# Store exception state for retrieval via get_install_progress_action()
self.bytes_downloaded = -1
def uninstall_package(self, name):
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
@ -70,12 +74,12 @@ class PackageManager:
with open(tmp_archive, 'wb') as f:
for chunk in r.iter_content(chunk_size=65536):
if chunk:
self.pending += f.write(chunk)
self.bytes_downloaded += f.write(chunk)
# Verify hash
if self.online_packages[name]['sha512'] != hash_file(tmp_archive):
raise InvalidSignature(name)
# Unpack
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/')
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
os.unlink(tmp_archive)
def purge_package(self, name):
@ -85,22 +89,6 @@ class PackageManager:
if os.path.exists(srv_dir):
shutil.rmtree(srv_dir)
def run_install_script(self, name):
# Runs install.sh for a package, if the script is present
install_dir = os.path.join('/srv/', name, 'install')
install_script = os.path.join('/srv/', name, 'install.sh')
if os.path.exists(install_script):
subprocess.run(install_script)
os.unlink(install_script)
if os.path.exists(install_dir):
shutil.rmtree(install_dir)
def run_uninstall_script(self, name):
# Runs uninstall.sh for a package, if the script is present
uninstall_script = os.path.join('/srv/', name, 'uninstall.sh')
if os.path.exists(uninstall_script):
subprocess.run(uninstall_script)
def register_package(self, name):
# Registers a package in local configuration
metadata = self.online_packages[name]
@ -127,6 +115,22 @@ class PackageManager:
del self.conf['apps'][name]
self.conf.save()
def run_install_script(self, name):
# Runs install.sh for a package, if the script is present
install_dir = os.path.join('/srv/', name, 'install')
install_script = os.path.join('/srv/', name, 'install.sh')
if os.path.exists(install_script):
subprocess.run(install_script, check=True)
os.unlink(install_script)
if os.path.exists(install_dir):
shutil.rmtree(install_dir)
def run_uninstall_script(self, name):
# Runs uninstall.sh for a package, if the script is present
uninstall_script = os.path.join('/srv/', name, 'uninstall.sh')
if os.path.exists(uninstall_script):
subprocess.run(uninstall_script, check=True)
def get_install_deps(self, name, online=True):
# Flatten dependency tree for a package while preserving the dependency order
packages = self.online_packages if online else self.conf['packages']

View File

@ -18,9 +18,6 @@ def is_valid_port(port):
except:
return False
def is_valid_app(app, conf):
return app in conf['apps']
def is_valid_email(email):
parts = email.split('@')
if len(parts) != 2:

View File

@ -154,9 +154,9 @@ class WSGIApp(object):
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, total_size=None):
def render_setup_apps_row(self, request, app, app_title, total_size=None, install_error=False):
t = self.jinja_env.get_template('setup-apps-row.html')
return t.render({'app': app, 'app_title': app_title, 'conf': self.conf, 'total_size': total_size})
return t.render({'conf': self.conf, 'session': request.session, 'app': app, 'app_title': app_title, 'total_size': total_size, 'install_error': install_error})
def update_host_action(self, request):
# Update domain and port, then restart nginx
@ -287,7 +287,7 @@ class WSGIApp(object):
except:
return self.render_json({'error': request.session.lang.stop_start_error()})
app_title = self.conf['apps'][app]['title']
return self.render_json({'ok': self.render_setup_apps_row(app, app_title)})
return self.render_json({'ok': self.render_setup_apps_row(request, app, app_title)})
def stop_app_action(self, request):
# Stops application along with its dependencies
@ -300,11 +300,11 @@ class WSGIApp(object):
except:
return self.render_json({'error': request.session.lang.stop_start_error()})
app_title = self.conf['apps'][app]['title']
return self.render_json({'ok': self.render_setup_apps_row(app, app_title)})
return self.render_json({'ok': self.render_setup_apps_row(request, app, app_title)})
def install_app_action(self, request):
# Registers the application installation as pending
if self.pkgmgr.pending:
if self.pkgmgr.bytes_downloaded > 0:
return self.render_json({'error': request.session.lang.installation_in_progress()})
try:
app = request.form['app']
@ -314,21 +314,23 @@ class WSGIApp(object):
except:
return self.render_json({'error': request.session.lang.package_manager_error()})
app_title = self.pkgmgr.online_packages[app]['title']
response = self.render_json({'ok': self.render_setup_apps_row(app, app_title, total_size)})
response = self.render_json({'ok': self.render_setup_apps_row(request, app, app_title, total_size)})
response.call_on_close(lambda: self.pkgmgr.install_package(app))
return response
def get_install_progress_action(self, request):
# Gets pending installation status
if self.pkgmgr.pending:
return self.render_json({'progress': self.pkgmgr.pending})
if self.pkgmgr.bytes_downloaded > 0:
return self.render_json({'progress': self.pkgmgr.bytes_downloaded})
app = request.form['app']
app_title = self.conf['apps'][app]['title']
return self.render_json({'ok': self.render_setup_apps_row(app, app_title)})
# In case of installation error, we need to get the name from online_packages as the app is not yet registered
app_title = self.conf['apps'][app]['title'] if app in self.conf['apps'] else self.pkgmgr.online_packages[app]['title']
install_error = True if self.pkgmgr.bytes_downloaded == -1 else False
return self.render_json({'ok': self.render_setup_apps_row(request, app, app_title, None, install_error)})
def uninstall_app_action(self, request):
# Uninstalls application
if self.pkgmgr.pending:
if self.pkgmgr.bytes_downloaded > 0:
return self.render_json({'error': request.session.lang.installation_in_progress()})
try:
app = request.form['app']
@ -337,9 +339,8 @@ class WSGIApp(object):
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
except:
raise
# return self.render_json({'error': request.session.lang.package_manager_error()})
return self.render_json({'ok': self.render_setup_apps_row(app, app_title)})
return self.render_json({'error': request.session.lang.package_manager_error()})
return self.render_json({'ok': self.render_setup_apps_row(request, app, app_title)})
def update_password_action(self, request):
# Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account

View File

@ -20,7 +20,7 @@ class WSGILang:
'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',
'package_manager_error': 'Došlo k chybě při instalaci aplikace. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'bad_password': 'Nesprávné heslo',
'password_mismatch': 'Zadaná hesla se neshodují',
'password_empty': 'Nové heslo nesmí být prázdné',

View File

@ -6,9 +6,9 @@ $(function() {
$('#cert-method').on('change', toggle_cert_method);
$('#update-cert').on('submit', update_cert);
$('#update-common').on('submit', update_common);
$('.app-visible').on('click', update_app_visibility);
$('.app-autostart').on('click', update_app_autostart);
$('#app-manager')
.on('click', '.app-visible', update_app_visibility)
.on('click', '.app-autostart', update_app_autostart)
.on('click', '.app-start', start_app)
.on('click', '.app-stop', stop_app)
.on('click', '.app-install', install_app)

View File

@ -1,7 +1,22 @@
<tr data-app="{{ app }}">
<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 total_size %}Stahuje se (<span id="install-progress" data-total="{{ total_size }}">1</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 total_size %}<div class="loader"></div>{% elif app not in conf['apps'] %}<a href="#" class="app-install">Instalovat</a>{% elif is_service_started(app) %}<a href="#" class="app-stop">Zastavit</a>{% else %}<a href="#" class="app-start">Spustit</a>, <a href="#" class="app-uninstall">Odinstalovat</a>{% endif %}</td>
{% set not_installed = app not in conf['apps'] %}
<td class="center"><input type="checkbox" class="app-visible"{% if not_installed %} disabled{% elif conf['apps'][app]['visible'] %} checked{% endif %}></td>
<td class="center"><input type="checkbox" class="app-autostart"{% if not_installed %} disabled{% elif is_service_autostarted(app) %} checked{% endif %}></td>
{% if install_error %}
<td>Není nainstalována</td>
<td><span class="error">{{ session.lang.package_manager_error() }}</span></td>
{% elif total_size %}
<td>Stahuje se (<span id="install-progress" data-total="{{ total_size }}">1</span> %)</td>
<td><div class="loader"></div></td>
{% elif not_installed %}
<td>Není nainstalována</td>
<td><a href="#" class="app-install">Instalovat</a></td>
{% elif is_service_started(app) %}
<td><span class="info">Spuštěna</span></td>
<td><a href="#" class="app-stop">Zastavit</a></td>
{% else %}
<td><span class="error">Zastavena</span></td>
<td><a href="#" class="app-start">Spustit</a>, <a href="#" class="app-uninstall">Odinstalovat</a></td>
{% endif %}
</tr>