diff --git a/etc/lxcmgr/packages.pub b/etc/lxcmgr/packages.pub deleted file mode 100644 index 60532d9..0000000 --- a/etc/lxcmgr/packages.pub +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWJXH4Qm0kt2L86sntQH+C1zOJNQ0qMRt -0vx4krTxRs9HQTQYAy//JC92ea2aKleA8OL0JF90b1NYXcQCWdAS+vE/ng9IEAii -8C2+5nfuFeZ5YUjbQhfFblwHSM0c7hEG ------END PUBLIC KEY----- diff --git a/etc/lxcmgr/repo.json b/etc/lxcmgr/repo.json deleted file mode 100644 index 41164e3..0000000 --- a/etc/lxcmgr/repo.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "url": "https://repo.spotter.cz/lxc", - "user": "", - "pwd": "" -} diff --git a/etc/vmmgr/config.default.json b/etc/vmmgr/config.json similarity index 100% rename from etc/vmmgr/config.default.json rename to etc/vmmgr/config.json diff --git a/usr/bin/lxchelper b/usr/bin/lxchelper deleted file mode 100755 index 8fe31d2..0000000 --- a/usr/bin/lxchelper +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -import argparse -import os -import subprocess -import sys -import tempfile - -from lxcmgr import lxcmgr -from lxcmgr.paths import LXC_ROOT - -def get_layers(container): - with open(os.path.join(LXC_ROOT, container, 'config')) as f: - for line in f.read().splitlines(): - if line.startswith('lxc.hook.pre-start'): - return line.split()[-1].split(',') - -def extract(args): - with tempfile.TemporaryDirectory() as tmp_rootfs: - layers = get_layers(args.container) - lxcmgr.mount_rootfs(args.container, layers, tmp_rootfs) - source = os.path.join(tmp_rootfs, args.source.lstrip('/')) - # Plain cp -pr as shutil.copytree() requires nonexistent target and copy2() doesn't retain owner - subprocess.run(['cp', '-pr', source, args.destination]) - lxcmgr.unmount_rootfs(tmp_rootfs) - -def main(args): - if args.action == 'prepare': - # Used with LXC hooks on container startup - lxcmgr.prepare_container(args.container, args.layers) - elif args.action == 'cleanup': - # Used with LXC hooks on container stop - lxcmgr.cleanup_container(args.container) - elif args.action == 'extract': - # Used in install.sh scripts to get files or directories from containers rootfs (excluding persistent mounts) - extract(args) - -parser = argparse.ArgumentParser(description='Collection of auxiliary LXC tools') -subparsers = parser.add_subparsers() - -parser_prepare = subparsers.add_parser('prepare', help='Perform pre-start steps for LXC') -parser_prepare.set_defaults(action='prepare') -parser_prepare.add_argument('layers', help='OverlayFS LXC rootfs layers') -parser_prepare.add_argument('container', help='Container name') -parser_prepare.add_argument('lxc', nargs=argparse.REMAINDER) - -parser_cleanup = subparsers.add_parser('cleanup', help='Perform post-stop steps for LXC') -parser_cleanup.set_defaults(action='cleanup') -parser_cleanup.add_argument('container', help='Container name') -parser_cleanup.add_argument('lxc', nargs=argparse.REMAINDER) - -parser_extract = subparsers.add_parser('extract', help='Extracts files or directories from containers rootfs (excluding persistent mounts)') -parser_extract.set_defaults(action='extract') -parser_extract.add_argument('container', help='Container name') -parser_extract.add_argument('source', help='Source file or directory within the container') -parser_extract.add_argument('destination', help='Destination file or directory on the host') - -args = parser.parse_args() -if hasattr(args, 'action'): - main(args) -else: - parser.print_usage() diff --git a/usr/bin/lxcmgr b/usr/bin/lxcmgr deleted file mode 100755 index 89e1582..0000000 --- a/usr/bin/lxcmgr +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -import argparse -import time -import sys - -from concurrent.futures import ThreadPoolExecutor - -from lxcmgr import lxcmgr -from lxcmgr.pkgmgr import App, Stage, PkgMgr - -def print_apps(packages): - for app, meta in packages.items(): - print(app, meta['version']) - for key, value in meta['meta'].items(): - print(' {}: {}'.format(key, value)) - -def list_online(): - pm = PkgMgr() - apps = pm.online_packages['apps'] - if apps: - print_apps(apps) - else: - print('Repository lists no applications packages.') - -def list_installed(): - pm = PkgMgr() - apps = pm.installed_packages['apps'] - if apps: - print_apps(apps) - else: - print('No applications packages installed.') - -def list_updates(): - pm = PkgMgr() - apps = pm.installed_packages['apps'] - if apps: - updateable_apps = [app for app in apps if pm.has_update(app)] - 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 run_install_action(action, app): - with ThreadPoolExecutor() as executor: - future = executor.submit(action, app) - while not future.done(): - time.sleep(0.25) - 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): - # Prints current status of the installation. Uses ANSI "erase line" and "carriage return" to rewrite the status on single line. - 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.UPDATE: - print('\x1b[KUpdating...', end='\r') - elif app.stage == Stage.DONE: - print('\x1b[KDone.') - -def install_app(app): - pm = PkgMgr() - app = App(app) - run_install_action(pm.install_app, app) - -def update_app(app): - pm = PkgMgr() - app = App(app) - run_install_action(pm.update_app, app) - -def uninstall_app(app): - pm = PkgMgr() - app = App(app) - run_install_action(pm.uninstall_app, app) - -def main(args): - if args.action == 'list-installed': - list_installed() - elif args.action == 'list-online': - list_online() - elif args.action == 'list-updates': - list_updates() - elif args.action == 'install': - install_app(args.app) - elif args.action == 'update': - update_app(args.app) - elif args.action == 'uninstall': - uninstall_app(args.app) - -parser = argparse.ArgumentParser(description='LXC container and package manager') -subparsers = parser.add_subparsers() - -parser_list = subparsers.add_parser('list') -parser_list.set_defaults(action='list-installed') -subparsers_list = parser_list.add_subparsers() -parser_list_installed = subparsers_list.add_parser('installed') -parser_list_installed.set_defaults(action='list-installed') -parser_list_online = subparsers_list.add_parser('online') -parser_list_online.set_defaults(action='list-online') -parser_list_updates = subparsers_list.add_parser('updates') -parser_list_updates.set_defaults(action='list-updates') - -parser_install = subparsers.add_parser('install') -parser_install.set_defaults(action='install') -parser_install.add_argument('app', help='Application to install') - -parser_update = subparsers.add_parser('update') -parser_update.set_defaults(action='update') -parser_update.add_argument('app', help='Application to update') - -parser_uninstall = subparsers.add_parser('uninstall') -parser_uninstall.set_defaults(action='uninstall') -parser_uninstall.add_argument('app', help='Application to uninstall') - -args = parser.parse_args() -if hasattr(args, 'action'): - main(args) -else: - parser.print_usage() diff --git a/usr/lib/python3.6/lxcmgr/__init__.py b/usr/lib/python3.6/lxcmgr/__init__.py deleted file mode 100644 index 40a96af..0000000 --- a/usr/lib/python3.6/lxcmgr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/usr/lib/python3.6/lxcmgr/crypto.py b/usr/lib/python3.6/lxcmgr/crypto.py deleted file mode 100644 index 5a169e3..0000000 --- a/usr/lib/python3.6/lxcmgr/crypto.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- - -import hashlib - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec - -def verify_signature(public_key_path, input_data, signature_data): - # Verifies ECDSA HMAC SHA512 signature of a file - with open(public_key_path, 'rb') as f: - pub_key = serialization.load_pem_public_key(f.read(), default_backend()) - pub_key.verify(signature_data, input_data, ec.ECDSA(hashes.SHA512())) - -def verify_hash(input_path, expected_hash): - # Verifies SHA512 hash of a file against expected hash - sha512 = hashlib.sha512() - with open(input_path, 'rb') as f: - while True: - data = f.read(65536) - if not data: - break - sha512.update(data) - if sha512.hexdigest() != expected_hash: - raise InvalidSignature(input_path) diff --git a/usr/lib/python3.6/lxcmgr/flock.py b/usr/lib/python3.6/lxcmgr/flock.py deleted file mode 100644 index 559e0f9..0000000 --- a/usr/lib/python3.6/lxcmgr/flock.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- - -import fcntl - -def flock_ex(lock_file): - def decorator(target): - def wrapper(*args, **kwargs): - with open(lock_file, 'w') as lock: - fcntl.lockf(lock, fcntl.LOCK_EX) - return target(*args, **kwargs) - return wrapper - return decorator diff --git a/usr/lib/python3.6/lxcmgr/lxcmgr.py b/usr/lib/python3.6/lxcmgr/lxcmgr.py deleted file mode 100644 index 248efa2..0000000 --- a/usr/lib/python3.6/lxcmgr/lxcmgr.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- - -import fcntl -import os -import shutil -import subprocess - -from . import flock -from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_LOGS, LXC_ROOT, LXC_STORAGE_DIR -from .templates import LXC_CONTAINER - -def prepare_container(container, layers): - # Remove ephemeral layer data - clean_ephemeral_layer(container) - # Prepare and mount overlayfs. This needs to be done before handing over control to LXC as we use unprivileged containers - # which don't have the capability to mount overlays - https://www.spinics.net/lists/linux-fsdevel/msg105877.html - rootfs = os.path.join(LXC_ROOT, container, 'rootfs') - # Unmount rootfs in case it remained mounted for whatever reason - unmount_rootfs(rootfs) - mount_rootfs(container, layers.split(','), rootfs) - -def mount_rootfs(container, layers, mountpoint): - if len(layers) == 1: - # We have only single layer, no overlay needed - subprocess.run(['mount', '--bind', layers[0], mountpoint]) - else: - olwork = os.path.join(LXC_ROOT, container, 'olwork') - subprocess.run(['mount', '-t', 'overlay', '-o', 'upperdir={},lowerdir={},workdir={}'.format(layers[-1], ':'.join(reversed(layers[:-1])), olwork), 'none', mountpoint]) - -def unmount_rootfs(mountpoint): - if os.path.exists(mountpoint): - subprocess.run(['umount', '--quiet', mountpoint]) - -def clean_ephemeral_layer(container): - # Cleans containers ephemeral layer. Called in lxc.hook.post-stop and lxc.hook.pre-start in case of unclean shutdown - # This is done early in the container start process, so the inode of the ephemeral directory must remain unchanged - ephemeral = os.path.join(LXC_ROOT, container, 'ephemeral') - for item in os.scandir(ephemeral): - shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path) - -def cleanup_container(container): - # Unmount rootfs - rootfs = os.path.join(LXC_ROOT, container, 'rootfs') - unmount_rootfs(rootfs) - # Remove ephemeral layer data - clean_ephemeral_layer(container) - -def create_container(container, image): - # Create directories after container installation - rootfs = os.path.join(LXC_ROOT, container, 'rootfs') - olwork = os.path.join(LXC_ROOT, container, 'olwork') - ephemeral = os.path.join(LXC_ROOT, container, 'ephemeral') - os.makedirs(rootfs, 0o755, True) - os.makedirs(olwork, 0o755, True) - os.makedirs(ephemeral, 0o755, True) - os.chown(ephemeral, 100000, 100000) - # Create container configuration file - layers = ','.join([os.path.join(LXC_STORAGE_DIR, layer) for layer in image['layers']]) - # Add ephemeral layer if the container is not created as part of build process - if 'build' not in image: - layers = '{},{}'.format(layers, ephemeral) - mounts = '\n{}'.format('\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{}'.format('\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' - gid = image['gid'] if 'gid' in image else '0' - cmd = image['cmd'] if 'cmd' in image else '/bin/sh' - cwd = image['cwd'] if 'cwd' in image else '/root' - halt = image['halt'] if 'halt' in image else 'SIGINT' - # Lease the first unused IP to the container - ipv4 = update_hosts_lease(container, True) - # Create the config file - with open(os.path.join(LXC_ROOT, container, 'config'), 'w') as f: - f.write(LXC_CONTAINER.format(name=container, ipv4=ipv4, layers=layers, mounts=mounts, env=env, uid=uid, gid=gid, cmd=cmd, cwd=cwd, halt=halt)) - -def destroy_container(container): - # Remove container configuration and directories - rootfs = os.path.join(LXC_ROOT, container, 'rootfs') - unmount_rootfs(rootfs) - try: - 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 - update_hosts_lease(container, False) - -@flock.flock_ex(HOSTS_LOCK) -def update_hosts_lease(container, is_request): - # This is a poor man's DHCP server which uses /etc/hosts as lease database - # Leases the first unused IP from range 172.17.0.0/16 - # Uses file lock as interprocess mutex - ip = None - # Load all existing records - with open(HOSTS_FILE, 'r') as f: - leases = [l.strip().split(' ', 1) for l in f] - # If this call is a request for lease, find if there isn't already existing lease for the container - if is_request: - already_leased = [l[0] for l in leases if l[1] == container] - if already_leased: - return already_leased[0] - # Otherwise assign the first unassigned IP - used_ips = [l[0] for l in leases] - for i in range(2, 65278): # Reserve last /24 subnet for VPN - ip = '172.17.{}.{}'. format(i // 256, i % 256) - if ip not in used_ips: - leases.append([ip, container]) - break - # Otherwise it is a release in which case we just delete the record - else: - leases = [l for l in leases if l[1] != container] - # Write the contents back to the file - with open(HOSTS_FILE, 'w') as f: - for lease in leases: - f.write('{} {}\n'.format(lease[0], lease[1])) - return ip diff --git a/usr/lib/python3.6/lxcmgr/paths.py b/usr/lib/python3.6/lxcmgr/paths.py deleted file mode 100644 index e7bc1eb..0000000 --- a/usr/lib/python3.6/lxcmgr/paths.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -# Package manager -REPO_CACHE_DIR = '/var/lib/lxcmgr/cache' -REPO_CONF_FILE = '/etc/lxcmgr/repo.json' -REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages' -REPO_LOCK = '/var/lock/lxcmgr-repo.lock' -REPO_SIG_FILE = '/etc/lxcmgr/packages.pub' - -# LXC -HOSTS_FILE = '/etc/hosts' -HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock' -LXC_LOGS = '/var/log/lxc' -LXC_ROOT = '/var/lib/lxc' -LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage' - -# Services -AUTOSTART_SVC_DIR = '/etc/runlevels/default' -SERVICE_DIR = '/etc/init.d' -STARTED_SVC_DIR = '/run/openrc/started' diff --git a/usr/lib/python3.6/lxcmgr/pkgmgr.py b/usr/lib/python3.6/lxcmgr/pkgmgr.py deleted file mode 100644 index e2b0de3..0000000 --- a/usr/lib/python3.6/lxcmgr/pkgmgr.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import os -import requests -import shutil -import subprocess - -from pkg_resources import parse_version - -from . import crypto -from . import flock -from . import lxcmgr -from . import svcmgr -from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_CONF_FILE, REPO_LOCAL_FILE, REPO_LOCK, REPO_SIG_FILE - -class Stage: - QUEUED = 1 - DOWNLOAD = 2 - UNPACK = 3 - INSTALL = 4 - UNINSTALL = 5 - UPDATE = 6 - DONE = 7 - -class RepoUnauthorized(Exception): - pass - -class RepoFileNotFound(Exception): - pass - -class RepoBadRequest(Exception): - pass - -class App: - def __init__(self, name): - self.name = name - self.stage = Stage.QUEUED - self.bytes_total = 1 - self.bytes_processed = 0 - - @property - def percent_processed(self): - # Limit the displayed percentage to 0 - 99 - return min(99, round(self.bytes_processed / self.bytes_total * 100)) - -class PkgMgr: - def __init__(self): - self.repo_url = None - self.repo_auth = None - self._online_packages = None - with open(REPO_LOCAL_FILE, 'r') as f: - self.installed_packages = json.load(f) - - @property - def online_packages(self): - if not self._online_packages: - # Fetches and verifies online packages. Can raise InvalidSignature - packages = self.get_repo_resource('packages').content - packages_sig = self.get_repo_resource('packages.sig').content - crypto.verify_signature(REPO_SIG_FILE, packages, packages_sig) - self._online_packages = json.loads(packages) - return self._online_packages - - def save_installed_packages(self): - with open(REPO_LOCAL_FILE, 'w') as f: - json.dump(self.installed_packages, f, sort_keys=True, indent=4) - - def load_repo_conf(self): - with open(REPO_CONF_FILE, 'r') as f: - conf = json.load(f) - self.repo_url = conf['url'] - user = conf['user'] if 'user' in conf and conf['user'] else None - pwd = conf['pwd'] if 'pwd' in conf and conf['pwd'] else None - self.repo_auth = (user, pwd) if user else None - - def get_repo_resource(self, resource_url, stream=False): - # Download requested repository resource - if not self.repo_url: - self.load_repo_conf() - # Make a HTTP request - r = requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream) - if r.status_code == 401: - raise RepoUnauthorized(r.url) - elif r.status_code == 404: - raise RepoFileNotFound(r.url) - elif r.status_code != 200: - raise RepoBadRequest(r.url) - return r - - @flock.flock_ex(REPO_LOCK) - def install_app(self, app): - # Main installation function. Wrapper for download, registration and install script - if app.name in self.installed_packages['apps']: - app.stage = Stage.DONE - return - self.download_and_unpack_deps(app) - # Run setup scripts - app.stage = Stage.INSTALL - # Run uninstall script to clean previous failed installation - self.run_script(app.name, 'uninstall') - # Build containers and services - self.create_containers(self.online_packages['apps'][app.name]['containers']) - # Run install script and register the app - self.run_script(app.name, 'install') - self.register_app(app.name, self.online_packages['apps'][app.name]) - app.stage = Stage.DONE - - def download_and_unpack_deps(self, app): - # Common download and unpack function for install and update - # Get all packages on which the app and its containers depend and which have not been installed yet - images = [] - image_deps = [container['image'] for container in self.online_packages['apps'][app.name]['containers'].values()] - for image in image_deps: - images.extend(self.online_packages['images'][image]['layers']) - images = [image for image in set(images) if image not in self.installed_packages['images']] - # Calculate bytes to download - app.bytes_total = sum(self.online_packages['images'][image]['pkgsize'] for image in images) + self.online_packages['apps'][app.name]['pkgsize'] - # Download layers and setup script files - app.stage = Stage.DOWNLOAD - for image in images: - self.download_image(app, image) - self.download_scripts(app) - # Purge old data to clean previous failed installation and unpack downloaded archives - app.stage = Stage.UNPACK - self.destroy_containers(self.online_packages['apps'][app.name]['containers']) - for image in images: - self.purge_image(image) - self.unpack_image(image) - self.register_image(image, self.online_packages['images'][image]) - self.purge_scripts(app.name) - self.unpack_scripts(app.name) - - def download_image(self, app, image): - # Download image archive and verify hash - archive = 'images/{}.tar.xz'.format(image) - self.download_archive(app, archive, self.online_packages['images'][image]['sha512']) - - def download_scripts(self, app): - # Download scripts archive and verify hash - archive = 'apps/{}.tar.xz'.format(app.name) - self.download_archive(app, archive, self.online_packages['apps'][app.name]['sha512']) - - def download_archive(self, app, archive, hash): - # Download the archive from online repository - tmp_archive = os.path.join(REPO_CACHE_DIR, archive) - res = self.get_repo_resource(archive, True) - with open(tmp_archive, 'wb') as f: - for chunk in res.iter_content(chunk_size=65536): - if chunk: - app.bytes_processed += f.write(chunk) - crypto.verify_hash(tmp_archive, hash) - - def purge_image(self, image): - # Delete layer files from storage directory - try: - shutil.rmtree(os.path.join(LXC_STORAGE_DIR, image)) - except FileNotFoundError: - pass - - def unpack_image(self, image): - # Unpack layer archive - tmp_archive = os.path.join(REPO_CACHE_DIR, 'images/{}.tar.xz'.format(image)) - subprocess.run(['tar', 'xJf', tmp_archive], cwd=LXC_STORAGE_DIR, check=True) - os.unlink(tmp_archive) - - def register_image(self, image, metadata): - # Add installed layer to list of installed images - self.installed_packages['images'][image] = metadata - self.save_installed_packages() - - def unregister_image(self, image): - # Remove image from list of installed images - if image in self.installed_packages['images']: - del self.installed_packages['images'][image] - self.save_installed_packages() - - def purge_scripts(self, app): - # Delete application setup scripts from storage directory - try: - shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app)) - except FileNotFoundError: - pass - - def unpack_scripts(self, app): - # Unpack setup scripts archive - tmp_archive = os.path.join(REPO_CACHE_DIR, 'apps/{}.tar.xz'.format(app)) - subprocess.run(['tar', 'xJf', tmp_archive], cwd=os.path.join(REPO_CACHE_DIR, 'apps'), check=True) - os.unlink(tmp_archive) - - def run_script(self, app, action): - # Runs script for an app, if the script is present - cache_dir = os.path.join(REPO_CACHE_DIR, 'apps', app) - script_dir = os.path.join(cache_dir, action) - script_path = '{}.sh'.format(script_dir) - if os.path.exists(script_path): - # Run the script in its working directory, if there is one, so it doesn't have to figure out paths to packaged files - cwd = script_dir if os.path.exists(script_dir) else cache_dir - subprocess.run(script_path, cwd=cwd, check=True) - - def register_app(self, app, metadata): - # Register installed app in list of installed apps - self.installed_packages['apps'][app] = metadata - self.save_installed_packages() - - def unregister_app(self, app): - # Remove app from list of installed apps - if app in self.installed_packages['apps']: - del self.installed_packages['apps'][app] - self.save_installed_packages() - - def create_containers(self, containers): - # Create LXC containers from image and app metadata - for container in containers: - image = containers[container]['image'] - image = self.online_packages['images'][image].copy() - if 'mounts' in containers[container]: - image['mounts'] = containers[container]['mounts'] - if 'depends' in containers[container]: - image['depends'] = containers[container]['depends'] - lxcmgr.create_container(container, image) - svcmgr.create_service(container, image) - svcmgr.update_services() - - def destroy_containers(self, containers): - # Destroy LXC containers - for container in containers: - svcmgr.delete_service(container) - lxcmgr.destroy_container(container) - svcmgr.update_services() - - @flock.flock_ex(REPO_LOCK) - def uninstall_app(self, app): - # Main uninstallation function. Wrapper for uninstall script and filesystem purge - if app.name not in self.installed_packages['apps']: - app.stage = Stage.DONE - return - app.stage = Stage.UNINSTALL - self.run_script(app.name, 'uninstall') - self.destroy_containers(self.installed_packages['apps'][app.name]['containers']) - self.purge_scripts(app.name) - self.unregister_app(app.name) - self.purge_unused_layers() - app.stage = Stage.DONE - - def purge_unused_layers(self): - # Remove layers which are no longer used by any installed application - layers = set(os.listdir(LXC_STORAGE_DIR)) - for app in self.installed_packages['apps']: - for container in self.installed_packages['apps'][app]['containers']: - image = self.installed_packages['apps'][app]['containers'][container]['image'] - for layer in self.installed_packages['images'][image]['layers']: - if layer in layers: - # Layer is still used, remove from set of layers to be purged - layers.remove(layer) - for layer in layers: - self.purge_image(layer) - - @flock.flock_ex(REPO_LOCK) - def update_app(self, app): - # Main update function. - self.download_and_unpack_deps(app) - # Run setup scripts - app.stage = Stage.UPDATE - # Build containers and services - self.create_containers(self.online_packages['apps'][app.name]['containers']) - # Run update script and register the app - self.run_script(app.name, 'update') - self.register_app(app.name, self.online_packages['apps'][app.name]) - app.stage = Stage.DONE - - def has_update(self, app): - # Check if online repository list a newer version of app - 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']) diff --git a/usr/lib/python3.6/lxcmgr/svcmgr.py b/usr/lib/python3.6/lxcmgr/svcmgr.py deleted file mode 100644 index bae2e65..0000000 --- a/usr/lib/python3.6/lxcmgr/svcmgr.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import subprocess - -from .paths import AUTOSTART_SVC_DIR, SERVICE_DIR, STARTED_SVC_DIR -from .templates import SERVICE - -def lxcize(service): - # Prepend lxc- to service name, otherwise there's much greater risk of naming conflict with other host services - return 'lxc-{}'.format(service) - -def create_service(container, image): - depends = ' '.join([lxcize(service) for service in image['depends']]) if 'depends' in image else '' - # Add ready check to start_post - # This could arguably be done better via some template engine, but introducing one for a single template file seems like an overkill - start_post = '\nstart_post() {{\n timeout -t 60 lxc-attach {} -- sh -c \'until {}; do sleep 0.1; done\'\n}}\n'.format(container, image['ready']) if 'ready' in image else '' - service_file = os.path.join(SERVICE_DIR, lxcize(container)) - with open(service_file, 'w') as f: - f.write(SERVICE.format(container=container, depends=depends, start_post=start_post)) - os.chmod(service_file, 0o755) - -def delete_service(service): - if is_service_started(service): - stop_service(service) - if is_service_autostarted(service): - update_service_autostart(service, False) - try: - os.unlink(os.path.join(SERVICE_DIR, lxcize(service))) - except FileNotFoundError: - pass - -def update_services(): - subprocess.run(['/sbin/rc-update', '-u'], check=True) - -def start_service(service): - if not is_service_started(service): - subprocess.run(['/sbin/service', lxcize(service), 'start'], check=True) - -def stop_service(service): - if is_service_started(service): - subprocess.run(['/sbin/service', lxcize(service), 'stop'], check=True) - -def is_service_started(service): - # Check OpenRC service status without calling any binary - return os.path.exists(os.path.join(STARTED_SVC_DIR, lxcize(service))) - -def is_service_autostarted(service): - # Check OpenRC service enablement - return os.path.exists(os.path.join(AUTOSTART_SVC_DIR, lxcize(service))) - -def update_service_autostart(service, enabled): - # Add/remove the service to/from OpenRC default runlevel - subprocess.run(['/sbin/rc-update', 'add' if enabled else 'del', lxcize(service)]) diff --git a/usr/lib/python3.6/lxcmgr/templates.py b/usr/lib/python3.6/lxcmgr/templates.py deleted file mode 100644 index 11ad81f..0000000 --- a/usr/lib/python3.6/lxcmgr/templates.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- - -LXC_CONTAINER = '''# Image name -lxc.uts.name = {name} - -# Network -lxc.net.0.type = veth -lxc.net.0.link = lxcbr0 -lxc.net.0.flags = up -lxc.net.0.ipv4.address = {ipv4}/16 -lxc.net.0.ipv4.gateway = 172.17.0.1 - -# Volumes -lxc.rootfs.path = /var/lib/lxc/{name}/rootfs - -# Mounts -lxc.mount.entry = shm dev/shm tmpfs rw,nodev,noexec,nosuid,relatime,mode=1777,create=dir 0 0 -lxc.mount.entry = /etc/hosts etc/hosts none bind,create=file 0 0 -lxc.mount.entry = /etc/resolv.conf etc/resolv.conf none bind,create=file 0 0{mounts} - -# Init -lxc.init.uid = {uid} -lxc.init.gid = {gid} -lxc.init.cwd = {cwd} -lxc.init.cmd = {cmd} - -# Environment -lxc.environment = PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin{env} - -# Halt -lxc.signal.halt = {halt} - -# Log -lxc.console.size = 1MB -lxc.console.logfile = /var/log/lxc/{name}.log - -# ID map -lxc.idmap = u 0 100000 65536 -lxc.idmap = g 0 100000 65536 - -# Hooks -lxc.hook.pre-start = /usr/bin/lxchelper prepare {layers} -lxc.hook.post-stop = /usr/bin/lxchelper cleanup - -# Other -lxc.arch = linux64 -lxc.include = /usr/share/lxc/config/common.conf -lxc.include = /usr/share/lxc/config/userns.conf -''' - -SERVICE = """#!/sbin/openrc-run - -description="{container} LXC container" - -depend() {{ - need cgroups {depends} -}} - -start() {{ - lxc-start {container} -}} -{start_post} -stop() {{ - lxc-stop {container} -}} -"""