Fix install, print CLI installation status
This commit is contained in:
parent
4c2616887f
commit
ce3fec4364
@ -2,9 +2,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from lxcmgr import lxcmgr
|
from lxcmgr import lxcmgr
|
||||||
from lxcmgr.pkgmgr import App, PkgMgr
|
from lxcmgr.pkgmgr import App, Stage, PkgMgr
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='LXC container and package manager')
|
parser = argparse.ArgumentParser(description='LXC container and package manager')
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
@ -53,25 +57,58 @@ def print_apps(packages):
|
|||||||
def list_online():
|
def list_online():
|
||||||
pm = PkgMgr()
|
pm = PkgMgr()
|
||||||
pm.fetch_online_packages()
|
pm.fetch_online_packages()
|
||||||
print_apps(pm.online_packages['apps'])
|
apps = pm.online_packages['apps']
|
||||||
|
if apps:
|
||||||
|
print_apps(apps)
|
||||||
|
else:
|
||||||
|
print('Repository lists no applications packages.')
|
||||||
|
|
||||||
def list_installed():
|
def list_installed():
|
||||||
pm = PkgMgr()
|
pm = PkgMgr()
|
||||||
pm.load_installed_packages()
|
apps = pm.installed_packages['apps']
|
||||||
print_apps(pm.installed_packages['apps'])
|
if apps:
|
||||||
|
print_apps(apps)
|
||||||
|
else:
|
||||||
|
print('No applications packages installed.')
|
||||||
|
|
||||||
def list_updates():
|
def list_updates():
|
||||||
pm = PkgMgr()
|
pm = PkgMgr()
|
||||||
pm.load_installed_packages()
|
apps = pm.installed_packages['apps']
|
||||||
apps = [app for app in pm.installed_packages if pm.has_update(app)]
|
if apps:
|
||||||
updates = {name: meta for (name, meta) in pm.online_packages['apps'].items() if name in apps}
|
updateable_apps = [app for app in apps if pm.has_update(app)]
|
||||||
print_apps(updates)
|
if updateable_apps:
|
||||||
|
updates = {name: meta for (name, meta) in pm.online_packages['apps'].items() if name in updateable_apps}
|
||||||
|
print_apps(updates)
|
||||||
|
else:
|
||||||
|
print('All installed application packages are up-to-date.')
|
||||||
|
else:
|
||||||
|
print('No applications packages installed.')
|
||||||
|
|
||||||
def install_app(app):
|
def install_app(app):
|
||||||
pm = PkgMgr()
|
pm = PkgMgr()
|
||||||
app = App(app)
|
app = App(app)
|
||||||
pm.install_app(app)
|
with ThreadPoolExecutor() as executor:
|
||||||
# TODO: periodicky vypisovat output
|
future = executor.submit(pm.install_app, app)
|
||||||
|
while not future.done():
|
||||||
|
time.sleep(1)
|
||||||
|
print_install_status(app)
|
||||||
|
# Get the result of the future and let it raise exception, if there was any
|
||||||
|
data = future.result()
|
||||||
|
print_install_status(app)
|
||||||
|
|
||||||
|
def print_install_status(app):
|
||||||
|
if app.stage == Stage.QUEUED:
|
||||||
|
print('\x1b[KQueued...', end='\r')
|
||||||
|
elif app.stage == Stage.DOWNLOAD:
|
||||||
|
print('\x1b[KDownloading... {} % ({} / {} bytes)'.format(app.percent_processed, app.bytes_processed, app.bytes_total), end='\r')
|
||||||
|
elif app.stage == Stage.UNPACK:
|
||||||
|
print('\x1b[KUnpacking...', end='\r')
|
||||||
|
elif app.stage == Stage.INSTALL:
|
||||||
|
print('\x1b[KInstalling...', end='\r')
|
||||||
|
elif app.stage == Stage.UNINSTALL:
|
||||||
|
print('\x1b[KUninstalling...', end='\r')
|
||||||
|
elif app.stage == Stage.DONE:
|
||||||
|
print('\x1b[KDone.')
|
||||||
|
|
||||||
def update_app(app):
|
def update_app(app):
|
||||||
pm = PkgMgr()
|
pm = PkgMgr()
|
||||||
@ -82,6 +119,10 @@ def uninstall_app(app):
|
|||||||
pm.uninstall_app(app)
|
pm.uninstall_app(app)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if not hasattr(args, 'action'):
|
||||||
|
parser.print_usage()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if args.action == 'list-installed':
|
if args.action == 'list-installed':
|
||||||
list_installed()
|
list_installed()
|
||||||
elif args.action == 'list-online':
|
elif args.action == 'list-online':
|
||||||
|
@ -50,7 +50,7 @@ def create_container(container, image):
|
|||||||
# Create container configuration file
|
# Create container configuration file
|
||||||
layers = ','.join([os.path.join(LXC_STORAGE_DIR, layer) for layer in image['layers']])
|
layers = ','.join([os.path.join(LXC_STORAGE_DIR, layer) for layer in image['layers']])
|
||||||
if 'build' not in image:
|
if 'build' not in image:
|
||||||
layers = '{},{}'.format(layer, ephemeral)
|
layers = '{},{}'.format(layers, ephemeral)
|
||||||
mounts = '\n'.join(['lxc.mount.entry = {} {} none bind,create={} 0 0'.format(m[1], m[2].lstrip('/'), m[0].lower()) for m in image['mounts']]) if 'mounts' in image else ''
|
mounts = '\n'.join(['lxc.mount.entry = {} {} none bind,create={} 0 0'.format(m[1], m[2].lstrip('/'), m[0].lower()) for m in image['mounts']]) if 'mounts' in image else ''
|
||||||
env = '\n'.join(['lxc.environment = {}={}'.format(e[0], e[1]) for e in image['env']]) if 'env' in image else ''
|
env = '\n'.join(['lxc.environment = {}={}'.format(e[0], e[1]) for e in image['env']]) if 'env' in image else ''
|
||||||
uid = image['uid'] if 'uid' in image else '0'
|
uid = image['uid'] if 'uid' in image else '0'
|
||||||
@ -66,8 +66,14 @@ def create_container(container, image):
|
|||||||
|
|
||||||
def destroy_container(container):
|
def destroy_container(container):
|
||||||
# Remove container configuration and directories
|
# Remove container configuration and directories
|
||||||
shutil.rmtree(os.path.join(LXC_ROOT, container))
|
try:
|
||||||
os.unlink(os.path.join(LXC_LOGS, '{}.log'.format(container)))
|
shutil.rmtree(os.path.join(LXC_ROOT, container))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.unlink(os.path.join(LXC_LOGS, '{}.log'.format(container)))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
# Release the IP address
|
# Release the IP address
|
||||||
update_hosts_lease(container, False)
|
update_hosts_lease(container, False)
|
||||||
|
|
||||||
|
@ -36,12 +36,12 @@ class App:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.stage = Stage.QUEUED
|
self.stage = Stage.QUEUED
|
||||||
self.bytes_total = 1
|
self.bytes_total = 1
|
||||||
self.bytes_downloaded = 0
|
self.bytes_processed = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percent_downloaded(self):
|
def percent_processed(self):
|
||||||
# Limit the displayed percentage to 0 - 99
|
# Limit the displayed percentage to 0 - 99
|
||||||
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
|
return min(99, round(self.bytes_processed / self.bytes_total * 100))
|
||||||
|
|
||||||
class PkgMgr:
|
class PkgMgr:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -92,23 +92,22 @@ class PkgMgr:
|
|||||||
return
|
return
|
||||||
if not self.online_packages:
|
if not self.online_packages:
|
||||||
self.fetch_online_packages()
|
self.fetch_online_packages()
|
||||||
# Get all packages on which the app depends and which have not been installed yet
|
# Get all packages on which the app and its containers depend and which have not been installed yet
|
||||||
#TODO: flatten and change name to "images"
|
images = []
|
||||||
layers = []
|
image_deps = [container['image'] for container in self.online_packages['apps'][app.name]['containers'].values()]
|
||||||
images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()]
|
for image in image_deps:
|
||||||
for image in images:
|
images.extend(self.online_packages['images'][image]['layers'])
|
||||||
layers.extend(self.online_packages['images'][image]['layers'])
|
images = [image for image in set(images) if image not in self.installed_packages['images']]
|
||||||
layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']]
|
|
||||||
# Calculate bytes to download
|
# Calculate bytes to download
|
||||||
app.bytes_total = sum(self.online_packages['images'][image]['size'] for image in layers) + self.online_packages['apps'][app.name]['size']
|
app.bytes_total = sum(self.online_packages['images'][image]['size'] for image in images) + self.online_packages['apps'][app.name]['size']
|
||||||
# Download layers and setup script files
|
# Download layers and setup script files
|
||||||
app.stage = Stage.DOWNLOAD
|
app.stage = Stage.DOWNLOAD
|
||||||
for image in layers:
|
for image in images:
|
||||||
self.download_image(app, image)
|
self.download_image(app, image)
|
||||||
self.download_scripts(app)
|
self.download_scripts(app)
|
||||||
# Purge old data to clean previous failed installation and unpack downloaded archives
|
# Purge old data to clean previous failed installation and unpack downloaded archives
|
||||||
app.stage = Stage.UNPACK
|
app.stage = Stage.UNPACK
|
||||||
for image in layers:
|
for image in images:
|
||||||
self.purge_image(image)
|
self.purge_image(image)
|
||||||
self.unpack_image(image)
|
self.unpack_image(image)
|
||||||
self.register_image(image, self.online_packages['images'][image])
|
self.register_image(image, self.online_packages['images'][image])
|
||||||
@ -128,28 +127,30 @@ class PkgMgr:
|
|||||||
|
|
||||||
def download_image(self, app, image):
|
def download_image(self, app, image):
|
||||||
# Download image archive and verify hash
|
# Download image archive and verify hash
|
||||||
pkg_archive = 'images/{}.tar.xz'.format(image)
|
archive = 'images/{}.tar.xz'.format(image)
|
||||||
self.download_archive(app, pkg_archive)
|
self.download_archive(app, archive, self.online_packages['images'][image]['sha512'])
|
||||||
crypto.verify_hash(tmp_archive, self.online_packages['images'][image]['sha512'])
|
|
||||||
|
|
||||||
def download_scripts(self, app):
|
def download_scripts(self, app):
|
||||||
# Download scripts archive and verify hash
|
# Download scripts archive and verify hash
|
||||||
pkg_archive = 'apps/{}.tar.xz'.format(app.name)
|
archive = 'apps/{}.tar.xz'.format(app.name)
|
||||||
self.download_archive(app, pkg_archive)
|
self.download_archive(app, archive, self.online_packages['apps'][app.name]['sha512'])
|
||||||
crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512'])
|
|
||||||
|
|
||||||
def download_archive(self, app, archive):
|
def download_archive(self, app, archive, hash):
|
||||||
# Download the archive from online repository
|
# Download the archive from online repository
|
||||||
tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive)
|
tmp_archive = os.path.join(REPO_CACHE_DIR, archive)
|
||||||
res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True)
|
res = self.get_repo_resource(archive, True)
|
||||||
with open(tmp_archive, 'wb') as f:
|
with open(tmp_archive, 'wb') as f:
|
||||||
for chunk in res.iter_content(chunk_size=65536):
|
for chunk in res.iter_content(chunk_size=65536):
|
||||||
if chunk:
|
if chunk:
|
||||||
app.bytes_downloaded += f.write(chunk)
|
app.bytes_processed += f.write(chunk)
|
||||||
|
crypto.verify_hash(tmp_archive, hash)
|
||||||
|
|
||||||
def purge_image(self, image):
|
def purge_image(self, image):
|
||||||
# Delete layer files from storage directory
|
# Delete layer files from storage directory
|
||||||
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, image))
|
try:
|
||||||
|
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, image))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def unpack_image(self, image):
|
def unpack_image(self, image):
|
||||||
# Unpack layer archive
|
# Unpack layer archive
|
||||||
@ -170,7 +171,10 @@ class PkgMgr:
|
|||||||
|
|
||||||
def purge_scripts(self, app):
|
def purge_scripts(self, app):
|
||||||
# Delete application setup scripts from storage directory
|
# Delete application setup scripts from storage directory
|
||||||
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app))
|
try:
|
||||||
|
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def unpack_scripts(self, app):
|
def unpack_scripts(self, app):
|
||||||
# Unpack setup scripts archive
|
# Unpack setup scripts archive
|
||||||
@ -188,7 +192,7 @@ class PkgMgr:
|
|||||||
|
|
||||||
def run_script(self, app, script):
|
def run_script(self, app, script):
|
||||||
# Runs script for an app, if the script is present
|
# Runs script for an app, if the script is present
|
||||||
script_path = os.path.join(REPO_CACHE_DIR, app, script)
|
script_path = os.path.join(REPO_CACHE_DIR, 'apps', app, script)
|
||||||
if os.path.exists(script_path):
|
if os.path.exists(script_path):
|
||||||
subprocess.run(script_path, check=True)
|
subprocess.run(script_path, check=True)
|
||||||
|
|
||||||
@ -207,7 +211,7 @@ class PkgMgr:
|
|||||||
# Create LXC containers from image and app metadata
|
# Create LXC containers from image and app metadata
|
||||||
for container in self.online_packages['apps'][app]['containers']:
|
for container in self.online_packages['apps'][app]['containers']:
|
||||||
image = self.online_packages['apps'][app]['containers'][container]['image']
|
image = self.online_packages['apps'][app]['containers'][container]['image']
|
||||||
image = self.online_packages['images'][image]['containers'].copy()
|
image = self.online_packages['images'][image].copy()
|
||||||
if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
|
if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
|
||||||
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
|
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
|
||||||
lxcmgr.create_container(container, image)
|
lxcmgr.create_container(container, image)
|
||||||
@ -248,8 +252,11 @@ class PkgMgr:
|
|||||||
install_app(app, item)
|
install_app(app, item)
|
||||||
|
|
||||||
def has_update(self, app):
|
def has_update(self, app):
|
||||||
if not self.installed_packages:
|
# Check if online repository list a newer version of app
|
||||||
self.load_installed_packages()
|
|
||||||
if not self.online_packages:
|
if not self.online_packages:
|
||||||
self.fetch_online_packages()
|
self.fetch_online_packages()
|
||||||
|
if app not in self.online_packages['apps']:
|
||||||
|
# Application has been removed from online repo
|
||||||
|
return False
|
||||||
|
# Compare version strings
|
||||||
return parse_version(self.installed_packages['apps'][app]['version']) < parse_version(self.online_packages['apps'][app]['version'])
|
return parse_version(self.installed_packages['apps'][app]['version']) < parse_version(self.online_packages['apps'][app]['version'])
|
||||||
|
@ -51,8 +51,11 @@ class VMMgr:
|
|||||||
|
|
||||||
def unregister_proxy(self, app):
|
def unregister_proxy(self, app):
|
||||||
# Remove proxy configuration and reload nginx
|
# Remove proxy configuration and reload nginx
|
||||||
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
try:
|
||||||
self.reload_nginx()
|
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
||||||
|
self.reload_nginx()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def update_host(self, domain, port):
|
def update_host(self, domain, port):
|
||||||
# Update domain and port, rebuild all configuration and restart nginx
|
# Update domain and port, rebuild all configuration and restart nginx
|
||||||
|
@ -229,7 +229,7 @@ class WSGIApp:
|
|||||||
actions = None
|
actions = None
|
||||||
else:
|
else:
|
||||||
if item.data.stage == Stage.DOWNLOAD:
|
if item.data.stage == Stage.DOWNLOAD:
|
||||||
status = '{} ({} %)'.format(lang.status_downloading(), item.data.percent_downloaded)
|
status = '{} ({} %)'.format(lang.status_downloading(), item.data.percent_processed)
|
||||||
elif item.data.stage == Stage.UNPACK:
|
elif item.data.stage == Stage.UNPACK:
|
||||||
status = lang.status_unpacking()
|
status = lang.status_unpacking()
|
||||||
elif item.data.stage == Stage.INSTALL_DEPS:
|
elif item.data.stage == Stage.INSTALL_DEPS:
|
||||||
|
Loading…
Reference in New Issue
Block a user