# -*- coding: utf-8 -*- import hashlib import json import os import requests import shutil import subprocess import tempfile from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_public_key PUB_FILE = '/srv/vm/packages.pub' LXC_ROOT = '/var/lib/lxc' class PackageManager: def __init__(self, conf): # Load JSON configuration self.conf = conf self.online_packages = {} self.pending = False self.pending_to_download = 0 self.pending_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) def fetch_online_packages(self): # Fetches and verifies online packages. Can raise InvalidSignature packages = self.get_repo_resource('packages').content packages_sig = self.get_repo_resource('packages.sig').content with open(PUB_FILE, 'rb') as f: pub_key = load_pem_public_key(f.read(), default_backend()) pub_key.verify(packages_sig, packages, ec.ECDSA(hashes.SHA512())) self.online_packages = json.loads(packages) def register_pending_installation(self): # 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 = True self.pending_to_download = 1 self.pending_downloaded = 0 def install_package(self, name): # Main installation function. Wrapper for download, registration and install script deps = d for d in self.get_deps(name) if d not in self.conf['packages'] self.pending_to_download = sum(self.online_packages[d]['size'] for d in deps) for dep in deps: self.download_package(dep) self.register_package(dep) self.run_install_script(dep) self.pending = False def uninstall_package(self, name): # Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration # TODO: Get dependencies which can be uninstalled self.run_uninstall_script(name) self.purge_package(name) self.unregister_package(name) def download_package(self, name): # Downloads, verifies, unpacks and sets up a package tmp_archive = tempfile.mkstemp('.tar.xz')[1] r = self.get_repo_resource('{}.tar.xz'.format(name), True) with open(tmp_archive, 'wb') as f: for chunk in r.iter_content(chunk_size=65536): if chunk: self.pending_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='/') os.unlink(tmp_archive) def purge_package(self, name): # Removes package and shared data from filesystem shutil.rmtree(os.path.join(LXC_ROOT, self.conf['packages'][name]['lxcpath'])) srv_dir = os.path.join('/srv/', name) if os.path.exsit(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] self.conf['packages'][name] = { 'deps': metadata['deps'], 'lxcpath': metadata['lxcpath'], 'version': metadata['version'] } # If host definition is present, register the package as application if 'host' in metadata: self.conf['apps'][name] = { 'title': metadata['title'], 'host': metadata['host'], 'login': 'N/A', 'password': 'N/A', 'visible': False } self.conf.save() def unregister_package(self, name): # Removes a package from local configuration del self.conf['packages'][name] if name in self.conf['apps']: del self.conf['apps'][name] self.conf.save() def get_deps(self, name): # Flatten dependency tree for a package deps = self.online_packages[name]['deps'].copy() for dep in deps: deps[:0] = [d for d in self.get_deps(dep) if d not in deps] deps.append(name) return deps def hash_file(file_path): sha512 = hashlib.sha512() with open(file_path, 'rb') as f: while True: data = f.read(65536) if not data: break sha512.update(data) return sha512.hexdigest()