Start with race condition mitigations
This commit is contained in:
parent
f675996e60
commit
25689d6345
@ -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()
|
||||
|
@ -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']
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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é',
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user