Split to lxcmgr
This commit is contained in:
parent
972ca0b696
commit
c3b711850e
118
usr/bin/lxcmgr
Normal file
118
usr/bin/lxcmgr
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from lxcmgr import lxcmgr
|
||||||
|
from lxcmgr.pkgmgr import App, PkgMgr
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='LXC container and package manager')
|
||||||
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
|
parser_list = subparsers.add_parser('list')
|
||||||
|
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')
|
||||||
|
|
||||||
|
parser_container = subparsers.add_parser('container')
|
||||||
|
subparsers_container = parser_container.add_subparsers()
|
||||||
|
|
||||||
|
parser_container_prepare = subparsers_container.add_parser('prepare')
|
||||||
|
parser_container_prepare.set_defaults(action='container-prepare')
|
||||||
|
parser_container_prepare.add_argument('layers', help='OverlayFS LXC rootfs layers')
|
||||||
|
parser_container_prepare.add_argument('container', help='Container name')
|
||||||
|
parser_container_prepare.add_argument('lxc', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
parser_container_cleanup = subparsers_container.add_parser('cleanup')
|
||||||
|
parser_container_cleanup.set_defaults(action='container-cleanup')
|
||||||
|
parser_container_cleanup.add_argument('container', help='Container name')
|
||||||
|
parser_container_cleanup.add_argument('lxc', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
parser_container_create = subparsers_container.add_parser('create')
|
||||||
|
parser_container_create.set_defaults(action='container-create')
|
||||||
|
parser_container_create.add_argument('container', help='Container name')
|
||||||
|
parser_container_create.add_argument('lxc', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
parser_container_destroy = subparsers_container.add_parser('destroy')
|
||||||
|
parser_container_destroy.set_defaults(action='container-destroy')
|
||||||
|
parser_container_destroy.add_argument('container', help='Container name')
|
||||||
|
parser_container_destroy.add_argument('lxc', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
def print_apps(packages):
|
||||||
|
for app, meta in packages.items():
|
||||||
|
print('{} {}'.format(app, meta['version']))
|
||||||
|
for key, value in meta['meta'].items():
|
||||||
|
print(' {}: {}'.format(key, value))
|
||||||
|
|
||||||
|
def list_online():
|
||||||
|
pm = PkgMgr()
|
||||||
|
pm.fetch_online_packages()
|
||||||
|
print_apps(pm.online_packages['apps'])
|
||||||
|
|
||||||
|
def list_installed():
|
||||||
|
pm = PkgMgr()
|
||||||
|
pm.load_installed_packages()
|
||||||
|
print_apps(pm.installed_packages['apps'])
|
||||||
|
|
||||||
|
def list_updates():
|
||||||
|
pm = PkgMgr()
|
||||||
|
pm.load_installed_packages()
|
||||||
|
apps = [app for app in pm.installed_packages if pm.has_update(app)]
|
||||||
|
updates = {name: meta for (name, meta) in pm.online_packages['apps'].items() if name in apps}
|
||||||
|
print_apps(updates)
|
||||||
|
|
||||||
|
def install_app(app):
|
||||||
|
pm = PkgMgr()
|
||||||
|
app = App(app)
|
||||||
|
pm.install_app(app)
|
||||||
|
# TODO: periodicky vypisovat output
|
||||||
|
|
||||||
|
def update_app(app):
|
||||||
|
pm = PkgMgr()
|
||||||
|
pm.update_app(app)
|
||||||
|
|
||||||
|
def uninstall_app(app):
|
||||||
|
pm = PkgMgr()
|
||||||
|
pm.uninstall_app(app)
|
||||||
|
|
||||||
|
args = parser.parse_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)
|
||||||
|
elif args.action == 'container-prepare':
|
||||||
|
# Used with LXC hooks on container startup
|
||||||
|
lxcmgr.prepare_container(args.container, args.layers)
|
||||||
|
elif args.action == 'container-cleanup':
|
||||||
|
# Used with LXC hooks on container stop
|
||||||
|
lxcmgr.cleanup_container(args.container)
|
||||||
|
elif args.action == 'container-create':
|
||||||
|
# Used by package installer and builder
|
||||||
|
lxcmgr.register_container(args.container)
|
||||||
|
elif args.action == 'container-destroy':
|
||||||
|
# Used by package installer and builder
|
||||||
|
lxcmgr.unregister_container(args.container)
|
@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from vmmgr import lxcmgr
|
|
||||||
from vmmgr.config import Config
|
from vmmgr.config import Config
|
||||||
from vmmgr.vmmgr import VMMgr
|
from vmmgr.vmmgr import VMMgr
|
||||||
|
|
||||||
@ -23,23 +23,6 @@ parser_unregister_app.add_argument('app', help='Application name')
|
|||||||
parser_rebuild_issue = subparsers.add_parser('rebuild-issue')
|
parser_rebuild_issue = subparsers.add_parser('rebuild-issue')
|
||||||
parser_rebuild_issue.set_defaults(action='rebuild-issue')
|
parser_rebuild_issue.set_defaults(action='rebuild-issue')
|
||||||
|
|
||||||
parser_prepare_container = subparsers.add_parser('prepare-container')
|
|
||||||
parser_prepare_container.set_defaults(action='prepare-container')
|
|
||||||
parser_prepare_container.add_argument('layers', help='OverlayFS LXC rootfs layers')
|
|
||||||
parser_prepare_container.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
parser_cleanup_container = subparsers.add_parser('cleanup-container')
|
|
||||||
parser_cleanup_container.set_defaults(action='cleanup-container')
|
|
||||||
parser_cleanup_container.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
parser_register_container = subparsers.add_parser('register-container')
|
|
||||||
parser_register_container.set_defaults(action='register-container')
|
|
||||||
parser_register_container.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
parser_unregister_container = subparsers.add_parser('unregister-container')
|
|
||||||
parser_unregister_container.set_defaults(action='unregister-container')
|
|
||||||
parser_unregister_container.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
parser_register_proxy = subparsers.add_parser('register-proxy')
|
parser_register_proxy = subparsers.add_parser('register-proxy')
|
||||||
parser_register_proxy.set_defaults(action='register-proxy')
|
parser_register_proxy.set_defaults(action='register-proxy')
|
||||||
parser_register_proxy.add_argument('app', help='Application name')
|
parser_register_proxy.add_argument('app', help='Application name')
|
||||||
@ -59,18 +42,6 @@ elif args.action == 'unregister-app':
|
|||||||
elif args.action == 'rebuild-issue':
|
elif args.action == 'rebuild-issue':
|
||||||
# Used by inittab on VM startup
|
# Used by inittab on VM startup
|
||||||
vmmgr.rebuild_issue()
|
vmmgr.rebuild_issue()
|
||||||
elif args.action == 'prepare-container':
|
|
||||||
# Used with LXC hooks on container startup
|
|
||||||
lxcmgr.prepare_container(args.layers)
|
|
||||||
elif args.action == 'cleanup-container':
|
|
||||||
# Used with LXC hooks on container stop
|
|
||||||
lxcmgr.cleanup_container()
|
|
||||||
elif args.action == 'register-container':
|
|
||||||
# Used by package installer and builder
|
|
||||||
lxcmgr.register_container()
|
|
||||||
elif args.action == 'unregister-container':
|
|
||||||
# Used by package installer and builder
|
|
||||||
lxcmgr.unregister_container()
|
|
||||||
elif args.action == 'register-proxy':
|
elif args.action == 'register-proxy':
|
||||||
# Used in init scripts on application startup
|
# Used in init scripts on application startup
|
||||||
vmmgr.register_proxy(args.app)
|
vmmgr.register_proxy(args.app)
|
||||||
|
1
usr/lib/python3.6/lxcmgr/__init__.py
Normal file
1
usr/lib/python3.6/lxcmgr/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
28
usr/lib/python3.6/lxcmgr/crypto.py
Normal file
28
usr/lib/python3.6/lxcmgr/crypto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
|
from .paths import REPO_SIG_FILE
|
||||||
|
|
||||||
|
def verify_signature(file, signature):
|
||||||
|
# Verifies ECDSA HMAC SHA512 signature of a file
|
||||||
|
with open(REPO_SIG_FILE, 'rb') as f:
|
||||||
|
pub_key = serialization.load_pem_public_key(f.read(), default_backend())
|
||||||
|
pub_key.verify(signature, file, ec.ECDSA(hashes.SHA512()))
|
||||||
|
|
||||||
|
def verify_hash(file, expected_hash):
|
||||||
|
# Verifies SHA512 hash of a file against expected hash
|
||||||
|
sha512 = hashlib.sha512()
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(65536)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha512.update(data)
|
||||||
|
if sha512.hexdigest() != expected_hash:
|
||||||
|
raise InvalidSignature(file)
|
12
usr/lib/python3.6/lxcmgr/flock.py
Normal file
12
usr/lib/python3.6/lxcmgr/flock.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# -*- 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
|
97
usr/lib/python3.6/lxcmgr/lxcmgr.py
Normal file
97
usr/lib/python3.6/lxcmgr/lxcmgr.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from . import flock
|
||||||
|
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT
|
||||||
|
from .templates import LXC_CONTAINER
|
||||||
|
|
||||||
|
def prepare_container(container, layers):
|
||||||
|
# Remove ephemeral layer data
|
||||||
|
clean_ephemeral_layer(container)
|
||||||
|
# Prepare and mount overlayfs
|
||||||
|
rootfs = os.path.join(LXC_ROOT, container, 'rootfs')
|
||||||
|
# Unmount rootfs in case it remained mounted for whatever reason
|
||||||
|
subprocess.run(['umount', rootfs])
|
||||||
|
layers = layers.split(',')
|
||||||
|
if len(layers) == 1:
|
||||||
|
# We have only single layer, no overlay needed
|
||||||
|
subprocess.run(['mount', '--bind', layers[0], rootfs])
|
||||||
|
else:
|
||||||
|
olwork = os.path.join(LXC_ROOT, container, 'olwork')
|
||||||
|
subprocess.run(['mount', '-t', 'overlay', '-o', 'upperdir={},lowerdir={},workdir={}'.format(layers[-1], ':'.join(layers[:-1]), olwork), 'none', rootfs])
|
||||||
|
|
||||||
|
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')
|
||||||
|
subprocess.run(['umount', 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']])
|
||||||
|
if 'build' not in image:
|
||||||
|
layers = '{},{}'.format(layer, 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 ''
|
||||||
|
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'
|
||||||
|
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 '/'
|
||||||
|
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
|
||||||
|
shutil.rmtree(os.path.join(LXC_ROOT, container))
|
||||||
|
# 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 the first unassigned IP
|
||||||
|
if is_request:
|
||||||
|
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
|
13
usr/lib/python3.6/lxcmgr/paths.py
Normal file
13
usr/lib/python3.6/lxcmgr/paths.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Package manager
|
||||||
|
REPO_CACHE_DIR = '/var/lib/lxcmgr/cache'
|
||||||
|
REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages'
|
||||||
|
REPO_LOCK = '/var/lock/lxcmgr-repo.lock'
|
||||||
|
REPO_SIG_FILE = '/var/lib/lxcmgr/packages.pub'
|
||||||
|
|
||||||
|
# LXC
|
||||||
|
HOSTS_FILE = '/etc/hosts'
|
||||||
|
HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock'
|
||||||
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
|
LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage'
|
229
usr/lib/python3.6/lxcmgr/pkgmgr.py
Normal file
229
usr/lib/python3.6/lxcmgr/pkgmgr.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
|
from . import crypto
|
||||||
|
from . import flock
|
||||||
|
from . import lxcmgr
|
||||||
|
from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_LOCAL_FILE, REPO_LOCK
|
||||||
|
|
||||||
|
class Stage(Enum):
|
||||||
|
QUEUED = 1
|
||||||
|
DOWNLOAD = 2
|
||||||
|
UNPACK = 3
|
||||||
|
INSTALL = 4
|
||||||
|
UNINSTALL = 5
|
||||||
|
DONE = 6
|
||||||
|
|
||||||
|
class RepoUnauthorized(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RepoFileNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RepoBadRequest(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AppInstall:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.stage = Stage.QUEUED
|
||||||
|
self.bytes_total = 1
|
||||||
|
self.bytes_downloaded = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percent_downloaded(self):
|
||||||
|
# Limit the displayed percentage to 0 - 99
|
||||||
|
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
|
||||||
|
|
||||||
|
class PkgMgr:
|
||||||
|
def __init__(self, repo_url, repo_auth=None):
|
||||||
|
self.repo_url = repo_url
|
||||||
|
self.repo_auth = repo_auth
|
||||||
|
self.installed_packages = None
|
||||||
|
self.online_packages = None
|
||||||
|
|
||||||
|
def load_installed_packages(self):
|
||||||
|
with open(REPO_LOCAL_FILE, 'r') as f:
|
||||||
|
self.installed_packages = json.load(f)
|
||||||
|
|
||||||
|
def save_installed_packages(self, packages):
|
||||||
|
with open(REPO_LOCAL_FILE, 'w') as f:
|
||||||
|
json.dump(packages, f, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
def get_repo_resource(self, resource_url, stream=False):
|
||||||
|
# Download requested repository resource
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
crypto.verify_signature(packages, packages_sig)
|
||||||
|
self.online_packages = json.loads(packages)
|
||||||
|
|
||||||
|
@flock.flock_ex(REPO_LOCK)
|
||||||
|
def install_app(self, app):
|
||||||
|
# Main installation function. Wrapper for download, registration and install script
|
||||||
|
if not self.installed_packages:
|
||||||
|
self.load_installed_packages()
|
||||||
|
# Request for installation of already installed app immediately returns with success
|
||||||
|
if app.name in self.installed_packages['apps']:
|
||||||
|
app.stage = Stage.DONE
|
||||||
|
return
|
||||||
|
if not self.online_packages:
|
||||||
|
self.fetch_online_packages()
|
||||||
|
# Get all packages on which the app depends and which have not been installed yet
|
||||||
|
layers = []
|
||||||
|
images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()]
|
||||||
|
for image in images:
|
||||||
|
layers.extend(self.online_packages['images'][image]['layers'])
|
||||||
|
layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']]
|
||||||
|
# Calculate bytes to download
|
||||||
|
app.bytes_total = sum(self.online_packages['images'][layer]['size'] for layer in layers) + self.online_packages['apps'][app.name]['size']
|
||||||
|
# Download layers and setup script files
|
||||||
|
app.stage = Stage.DOWNLOAD
|
||||||
|
for layer in layers:
|
||||||
|
self.download_layer(app, layer)
|
||||||
|
self.download_scripts(app)
|
||||||
|
# Purge old data (to clean previous failed installation) and unpack
|
||||||
|
app.stage = Stage.UNPACK
|
||||||
|
for layer in layers:
|
||||||
|
self.purge_layer(layer)
|
||||||
|
self.unpack_layer(layer)
|
||||||
|
self.purge_scripts(app.name)
|
||||||
|
self.unpack_scripts(app.name)
|
||||||
|
# Run setup scripts
|
||||||
|
app.stage = Stage.INSTALL
|
||||||
|
self.run_uninstall_script(app.name)
|
||||||
|
# Build containers and services
|
||||||
|
self.create_containers(app.name)
|
||||||
|
# Run install script and finish the installation
|
||||||
|
self.run_install_script(app.name)
|
||||||
|
self.installed_packages['apps'][app.name] = self.online_packages['apps'][app.name]
|
||||||
|
self.save_installed_packages()
|
||||||
|
app.stage = Stage.DONE
|
||||||
|
|
||||||
|
def download_layer(self, app, layer):
|
||||||
|
pkg_archive = 'images/{}.tar.xz'.format(layer)
|
||||||
|
self.download_archive(app, pkg_archive)
|
||||||
|
# Verify hash
|
||||||
|
crypto.verify_hash(tmp_archive, self.online_packages['images'][layer]['sha512'])
|
||||||
|
|
||||||
|
def download_scripts(self, app):
|
||||||
|
pkg_archive = 'apps/{}.tar.xz'.format(app.name)
|
||||||
|
self.download_archive(app, pkg_archive)
|
||||||
|
# Verify hash
|
||||||
|
crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512'])
|
||||||
|
|
||||||
|
def download_archive(self, app, archive):
|
||||||
|
# Download the archive
|
||||||
|
tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive)
|
||||||
|
res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True)
|
||||||
|
with open(tmp_archive, 'wb') as f:
|
||||||
|
for chunk in res.iter_content(chunk_size=65536):
|
||||||
|
if chunk:
|
||||||
|
app.bytes_downloaded += f.write(chunk)
|
||||||
|
|
||||||
|
def purge_layer(self, layer):
|
||||||
|
# Delete layer files from storage directory
|
||||||
|
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, layer))
|
||||||
|
if layer in self.installed_packages['images']:
|
||||||
|
del self.installed_packages['images'][layer]
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
|
def unpack_layer(self, layer):
|
||||||
|
# Unpack layer archive
|
||||||
|
tmp_archive = os.path.join(REPO_CACHE_DIR, 'images/{}.tar.xz'.format(layer))
|
||||||
|
subprocess.run(['tar', 'xJf', tmp_archive], cwd=LXC_STORAGE_DIR, check=True)
|
||||||
|
os.unlink(tmp_archive)
|
||||||
|
self.installed_packages['images'][layer] = self.online_packages['images'][layer]
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
|
def purge_scripts(self, app):
|
||||||
|
# Delete application setup scripts from storage directory
|
||||||
|
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app))
|
||||||
|
del self.installed_packages['apps'][app]
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
|
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_uninstall_script(self, app):
|
||||||
|
# Runs uninstall.sh for an app, if the script is present
|
||||||
|
self.run_script(app, 'uninstall.sh')
|
||||||
|
|
||||||
|
def run_install_script(self, app):
|
||||||
|
# Runs install.sh for a package, if the script is present
|
||||||
|
self.run_script(app, 'install.sh')
|
||||||
|
|
||||||
|
def run_script(self, app, script):
|
||||||
|
# Runs script for an app, if the script is present
|
||||||
|
script_path = os.path.join(REPO_CACHE_DIR, app, script)
|
||||||
|
if os.path.exists(script_path):
|
||||||
|
subprocess.run(script_path, check=True)
|
||||||
|
|
||||||
|
def create_containers(self, app):
|
||||||
|
# Create LXC containers from image and app metadata
|
||||||
|
for container in self.online_packages['apps'][app]['containers']:
|
||||||
|
image = self.online_packages['apps'][app]['containers'][container]['image']
|
||||||
|
image = self.online_packages['images'][image]['containers'].copy()
|
||||||
|
if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
|
||||||
|
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
|
||||||
|
lxcmgr.create_container(container, image)
|
||||||
|
|
||||||
|
@flock.flock_ex(REPO_LOCK)
|
||||||
|
def uninstall_app(self, app):
|
||||||
|
# Main uninstallation function. Wrapper for uninstall script and filesystem purge
|
||||||
|
self.run_uninstall_script(app)
|
||||||
|
self.destroy_containers(app)
|
||||||
|
self.purge_scripts(app)
|
||||||
|
self.purge_unused_layers()
|
||||||
|
|
||||||
|
def destroy_containers(self, app):
|
||||||
|
# Destroy LXC containers
|
||||||
|
for container in self.installed_packages['apps'][app]['containers']:
|
||||||
|
lxcmgr.destroy_container(container)
|
||||||
|
|
||||||
|
def purge_unused_layers(self):
|
||||||
|
# Remove layers which are no longer used by any installed application
|
||||||
|
layers = set(os.list(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:
|
||||||
|
del layers[layer]
|
||||||
|
for layer in layers:
|
||||||
|
self.purge_layer(layer)
|
||||||
|
|
||||||
|
@flock.flock_ex(REPO_LOCK)
|
||||||
|
def update_app(self, app, item):
|
||||||
|
# Main update function.
|
||||||
|
# TODO: Implement actual update
|
||||||
|
#uninstall_app(app)
|
||||||
|
#install_app(app, item)
|
||||||
|
|
||||||
|
def has_update(self, app):
|
||||||
|
if not self.installed_packages:
|
||||||
|
self.load_installed_packages()
|
||||||
|
if not self.online_packages:
|
||||||
|
self.fetch_online_packages()
|
||||||
|
return parse_version(self.installed_packages['apps'][app]['version']) < parse_version(self.online_packages['apps'][app]['version'])
|
51
usr/lib/python3.6/lxcmgr/templates.py
Normal file
51
usr/lib/python3.6/lxcmgr/templates.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- 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}
|
||||||
|
|
||||||
|
# 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/vmmgr prepare-container {layers}
|
||||||
|
lxc.hook.post-stop = /usr/bin/vmmgr cleanup-container
|
||||||
|
|
||||||
|
# Other
|
||||||
|
lxc.arch = linux64
|
||||||
|
lxc.cap.drop = sys_admin
|
||||||
|
lxc.include = /usr/share/lxc/config/common.conf
|
||||||
|
lxc.include = /usr/share/lxc/config/userns.conf
|
||||||
|
'''
|
@ -2,35 +2,15 @@
|
|||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.exceptions import InvalidSignature
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
|
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
|
||||||
|
|
||||||
from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE, PKG_SIG_FILE
|
from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE
|
||||||
|
|
||||||
def verify_signature(file, signature):
|
|
||||||
# Verifies ECDSA HMAC SHA512 signature of a file
|
|
||||||
with open(PKG_SIG_FILE, 'rb') as f:
|
|
||||||
pub_key = serialization.load_pem_public_key(f.read(), default_backend())
|
|
||||||
pub_key.verify(signature, file, ec.ECDSA(hashes.SHA512()))
|
|
||||||
|
|
||||||
def verify_hash(file, expected_hash):
|
|
||||||
# Verifies SHA512 hash of a file against expected hash
|
|
||||||
sha512 = hashlib.sha512()
|
|
||||||
with open(file, 'rb') as f:
|
|
||||||
while True:
|
|
||||||
data = f.read(65536)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
sha512.update(data)
|
|
||||||
if sha512.hexdigest() != expected_hash:
|
|
||||||
raise InvalidSignature(file)
|
|
||||||
|
|
||||||
def create_selfsigned_cert(domain):
|
def create_selfsigned_cert(domain):
|
||||||
# Create selfsigned certificate with wildcard alternative subject name
|
# Create selfsigned certificate with wildcard alternative subject name
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import fcntl
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT
|
|
||||||
from .templates import LXC_CONTAINER
|
|
||||||
|
|
||||||
def prepare_container(layers):
|
|
||||||
# Extract the variables from values given via lxc.hook.pre-start hook
|
|
||||||
app = os.environ['LXC_NAME']
|
|
||||||
# Remove ephemeral layer data
|
|
||||||
clean_ephemeral_layer(app)
|
|
||||||
# Prepare and mount overlayfs
|
|
||||||
prepare_overlayfs(app, layers)
|
|
||||||
# Configure host and common params used in the app
|
|
||||||
configure_app(app)
|
|
||||||
|
|
||||||
def clean_ephemeral_layer(app):
|
|
||||||
# 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, app, 'ephemeral')
|
|
||||||
for item in os.scandir(ephemeral):
|
|
||||||
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
|
|
||||||
|
|
||||||
def prepare_overlayfs(app, layers):
|
|
||||||
# Prepare and mount overlayfs
|
|
||||||
rootfs = os.path.join(LXC_ROOT, app, 'rootfs')
|
|
||||||
# Unmount rootfs in case it remained mounted for whatever reason
|
|
||||||
subprocess.run(['umount', rootfs])
|
|
||||||
layers = layers.split(',')
|
|
||||||
if len(layers) == 1:
|
|
||||||
# We have only single layer, no overlay needed
|
|
||||||
subprocess.run(['mount', '--bind', layers[0], rootfs])
|
|
||||||
else:
|
|
||||||
olwork = os.path.join(LXC_ROOT, app, 'olwork')
|
|
||||||
subprocess.run(['mount', '-t', 'overlay', '-o', 'upperdir={},lowerdir={},workdir={}'.format(layers[-1], ','.join(layers[:-1]), olwork), 'none', rootfs])
|
|
||||||
|
|
||||||
def configure_app(app):
|
|
||||||
# Supply common configuration for the application. Done as part of container preparation during service startup
|
|
||||||
script = os.path.join('/srv', app, 'update-conf.sh')
|
|
||||||
if os.path.exists(script):
|
|
||||||
conf = Config()
|
|
||||||
setup_env = os.environ.copy()
|
|
||||||
setup_env['DOMAIN'] = conf['host']['domain']
|
|
||||||
setup_env['PORT'] = conf['host']['port']
|
|
||||||
setup_env['EMAIL'] = conf['common']['email']
|
|
||||||
setup_env['GMAPS_API_KEY'] = conf['common']['gmaps-api-key']
|
|
||||||
subprocess.run([script], env=setup_env, check=True)
|
|
||||||
|
|
||||||
def cleanup_container():
|
|
||||||
# Extract the variables from values given via lxc.hook.post-stop hook
|
|
||||||
app = os.environ['LXC_NAME']
|
|
||||||
# Unmount rootfs
|
|
||||||
rootfs = os.path.join(LXC_ROOT, app, 'rootfs')
|
|
||||||
subprocess.run(['umount', rootfs])
|
|
||||||
# Remove ephemeral layer data
|
|
||||||
clean_ephemeral_layer(app)
|
|
||||||
|
|
||||||
def register_container(app, image):
|
|
||||||
# Create directories after container installation
|
|
||||||
rootfs = os.path.join(LXC_ROOT, app, 'rootfs')
|
|
||||||
olwork = os.path.join(LXC_ROOT, app, 'olwork')
|
|
||||||
ephemeral = os.path.join(LXC_ROOT, app, '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_ROOT, 'storage', layer) for layer in image['layers']])
|
|
||||||
if 'build' not in image:
|
|
||||||
layers = '{},{}'.format(layer, ephemeral)
|
|
||||||
mounts = '\n'.join(['lxc.mount.entry = {} {} none bind,create={} 0 0'.format(m[1], m[2], 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 ''
|
|
||||||
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 '/'
|
|
||||||
halt = image['halt'] if 'halt' in image else 'SIGINT'
|
|
||||||
# Lease the first unused IP to the container
|
|
||||||
ipv4 = update_hosts_lease(app, True)
|
|
||||||
# Create the config file
|
|
||||||
with open(os.path.join(LXC_ROOT, app, 'config'), 'w') as f:
|
|
||||||
f.write(LXC_CONTAINER.format(name=app, ipv4=ipv4, layers=layers, mounts=mounts, env=env, uid=uid, gid=gid, cmd=cmd, cwd=cwd, halt=halt))
|
|
||||||
|
|
||||||
def unregister_container(app):
|
|
||||||
# Remove container configuration and directories
|
|
||||||
# TODO: Duplicated with what pkgmgr does, ale zustane to tady, protoze unregister se pouziva pri buildu
|
|
||||||
shutil.rmtree(os.path.join(LXC_ROOT, app))
|
|
||||||
# Release the IP address
|
|
||||||
update_hosts_lease(app, False)
|
|
||||||
|
|
||||||
def update_hosts_lease(app, 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
|
|
||||||
with open(HOSTS_LOCK, 'w') as lock:
|
|
||||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
|
||||||
# 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 the first unassigned IP
|
|
||||||
if is_request:
|
|
||||||
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, app])
|
|
||||||
break
|
|
||||||
# Otherwise it is a release in which case we just delete the record
|
|
||||||
else:
|
|
||||||
leases = [l for l in leases if l[1] != app]
|
|
||||||
# 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
|
|
@ -11,13 +11,15 @@ CERT_KEY_FILE = '/etc/ssl/services.key'
|
|||||||
CERT_PUB_FILE = '/etc/ssl/services.pem'
|
CERT_PUB_FILE = '/etc/ssl/services.pem'
|
||||||
|
|
||||||
# Package manager
|
# Package manager
|
||||||
PKG_SIG_FILE = '/etc/vmmgr/packages.pub'
|
REPO_LOCAL_FILE = '/var/lib/lxc-pkg/packages'
|
||||||
PKG_TEMP_DIR = '/var/cache/vmmgr'
|
REPO_SIG_FILE = '/etc/vmmgr/packages.pub'
|
||||||
|
REPO_CACHE_DIR = '/var/lib/lxc-pkg/cache'
|
||||||
|
|
||||||
# LXC
|
# LXC
|
||||||
HOSTS_FILE = '/etc/hosts'
|
HOSTS_FILE = '/etc/hosts'
|
||||||
HOSTS_LOCK = '/var/lock/vmmgr-hosts.lock'
|
HOSTS_LOCK = '/var/lock/vmmgr-hosts.lock'
|
||||||
LXC_ROOT = '/var/lib/lxc'
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
|
LXC_STORAGE_DIR = '/var/lib/lxc-pkg/storage'
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
ISSUE_FILE = '/etc/issue'
|
ISSUE_FILE = '/etc/issue'
|
||||||
|
@ -1,178 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from pkg_resources import parse_version
|
|
||||||
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
|
||||||
|
|
||||||
from . import crypto
|
|
||||||
from .paths import LXC_ROOT, PKG_TEMP_DIR
|
|
||||||
|
|
||||||
class Stage(Enum):
|
|
||||||
DOWNLOAD = 1
|
|
||||||
UNPACK = 2
|
|
||||||
INSTALL_DEPS = 3
|
|
||||||
INSTALL_APP = 4
|
|
||||||
|
|
||||||
class Pkg:
|
|
||||||
def __init__(self):
|
|
||||||
self.stage = Stage.DOWNLOAD
|
|
||||||
self.bytes_total = 1
|
|
||||||
self.bytes_downloaded = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def percent_downloaded(self):
|
|
||||||
# Limit the displayed percentage to 0 - 99
|
|
||||||
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
|
|
||||||
|
|
||||||
class PkgMgr:
|
|
||||||
def __init__(self, conf):
|
|
||||||
self.conf = conf
|
|
||||||
self.online_packages = {}
|
|
||||||
|
|
||||||
def get_repo_resource(self, resource_url, stream=False):
|
|
||||||
# Download requested repository resource
|
|
||||||
r = requests.get('{}/{}'.format(self.conf['repo']['url'], resource_url), auth=(self.conf['repo']['user'], self.conf['repo']['pwd']), timeout=5, stream=stream)
|
|
||||||
if r.status_code == 401:
|
|
||||||
raise Unauthorized(r.url)
|
|
||||||
elif r.status_code == 404:
|
|
||||||
raise NotFound(r.url)
|
|
||||||
elif r.status_code != 200:
|
|
||||||
raise BadRequest(r.url)
|
|
||||||
return r
|
|
||||||
|
|
||||||
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
|
|
||||||
crypto.verify_signature(packages, packages_sig)
|
|
||||||
self.online_packages = json.loads(packages)
|
|
||||||
|
|
||||||
def install_app(self, app, item):
|
|
||||||
# Main installation function. Wrapper for download, registration and install script
|
|
||||||
self.fetch_online_packages()
|
|
||||||
# Get all packages on which the app depends and which have not been installed yet
|
|
||||||
deps = [d for d in self.get_install_deps(app) if d not in self.conf['packages']]
|
|
||||||
item.bytes_total = sum(self.online_packages[d]['size'] for d in deps)
|
|
||||||
for dep in deps:
|
|
||||||
self.download_package(dep, item)
|
|
||||||
for dep in deps:
|
|
||||||
# Purge old data before unpacking to clean previous failed installation
|
|
||||||
item.stage = Stage.UNPACK
|
|
||||||
self.purge_package(dep)
|
|
||||||
self.unpack_package(dep)
|
|
||||||
for dep in deps:
|
|
||||||
# Set stage to INSTALLING_DEPS or INSTALLING based on which package in sequence is being installed
|
|
||||||
item.stage = Stage.INSTALL_APP if dep == deps[-1] else Stage.INSTALL_DEPS
|
|
||||||
# Run uninstall script before installation to clean previous failed installation
|
|
||||||
self.run_uninstall_script(dep)
|
|
||||||
self.run_install_script(dep)
|
|
||||||
self.register_package(dep)
|
|
||||||
|
|
||||||
def uninstall_app(self, app):
|
|
||||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
|
||||||
deps = self.get_install_deps(app, False)[::-1]
|
|
||||||
for dep in deps:
|
|
||||||
if dep not in self.get_uninstall_deps():
|
|
||||||
self.run_uninstall_script(dep)
|
|
||||||
self.purge_package(dep)
|
|
||||||
self.unregister_package(dep)
|
|
||||||
|
|
||||||
def update_app(self, app, item):
|
|
||||||
# Main update function.
|
|
||||||
# TODO: Implement actual update
|
|
||||||
uninstall_app(app)
|
|
||||||
install_app(app, item)
|
|
||||||
|
|
||||||
def download_package(self, name, item):
|
|
||||||
# Download tar.xz package and verify its hash. Can raise InvalidSignature
|
|
||||||
pkg_archive = '{}_{}-{}.tar.xz'.format(name, self.online_packages[name]['version'], self.online_packages[name]['release'])
|
|
||||||
tmp_archive = os.path.join(PKG_TEMP_DIR, pkg_archive)
|
|
||||||
os.makedirs(PKG_TEMP_DIR, 0o700, True)
|
|
||||||
# If the archive already exists in temp (presumably because the previous installation was interrupted), it was already verified and can be reused
|
|
||||||
if os.path.exists(tmp_archive):
|
|
||||||
item.bytes_downloaded += os.path.getsize(tmp_archive)
|
|
||||||
return
|
|
||||||
# Download the archive
|
|
||||||
partial_archive = '{}.partial'.format(tmp_archive)
|
|
||||||
res = self.get_repo_resource(pkg_archive, True)
|
|
||||||
with open(partial_archive, 'wb') as f:
|
|
||||||
for chunk in res.iter_content(chunk_size=65536):
|
|
||||||
if chunk:
|
|
||||||
item.bytes_downloaded += f.write(chunk)
|
|
||||||
# Verify hash
|
|
||||||
crypto.verify_hash(partial_archive, self.online_packages[name]['sha512'])
|
|
||||||
# Remove ".partial" extension
|
|
||||||
os.rename(partial_archive, tmp_archive)
|
|
||||||
|
|
||||||
def unpack_package(self, name):
|
|
||||||
# Unpack archive
|
|
||||||
pkg_archive = '{}_{}-{}.tar.xz'.format(name, self.online_packages[name]['version'], self.online_packages[name]['release'])
|
|
||||||
tmp_archive = os.path.join(PKG_TEMP_DIR, pkg_archive)
|
|
||||||
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
|
|
||||||
os.unlink(tmp_archive)
|
|
||||||
|
|
||||||
def purge_package(self, name):
|
|
||||||
# Removes package and shared data from filesystem
|
|
||||||
lxcpath = self.conf['packages'][name]['lxcpath'] if name in self.conf['packages'] else self.online_packages[name]['lxcpath']
|
|
||||||
lxc_dir = os.path.join(LXC_ROOT, lxcpath)
|
|
||||||
if os.path.exists(lxc_dir):
|
|
||||||
shutil.rmtree(lxc_dir)
|
|
||||||
srv_dir = os.path.join('/srv/', name)
|
|
||||||
if os.path.exists(srv_dir):
|
|
||||||
shutil.rmtree(srv_dir)
|
|
||||||
lxc_log = '/var/log/lxc/{}.log'.format(name)
|
|
||||||
if os.path.exists(lxc_log):
|
|
||||||
os.unlink(lxc_log)
|
|
||||||
|
|
||||||
def register_package(self, name):
|
|
||||||
# Registers a package in installed packages
|
|
||||||
metadata = self.online_packages[name].copy()
|
|
||||||
del metadata['sha512']
|
|
||||||
del metadata['size']
|
|
||||||
self.conf['packages'][name] = metadata
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def unregister_package(self, name):
|
|
||||||
# Removes a package from installed packages
|
|
||||||
del self.conf['packages'][name]
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def run_install_script(self, name):
|
|
||||||
# Runs install.sh for a package, if the script is present
|
|
||||||
install_script = os.path.join('/srv/', name, 'install.sh')
|
|
||||||
if os.path.exists(install_script):
|
|
||||||
subprocess.run(install_script, check=True)
|
|
||||||
|
|
||||||
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']
|
|
||||||
deps = packages[name]['depends'].copy()
|
|
||||||
for dep in deps[::-1]:
|
|
||||||
deps[:0] = [d for d in self.get_install_deps(dep, online)]
|
|
||||||
deps = list(dict.fromkeys(deps + [name]))
|
|
||||||
return deps
|
|
||||||
|
|
||||||
def get_uninstall_deps(self):
|
|
||||||
# Create reverse dependency tree for all installed packages
|
|
||||||
deps = {}
|
|
||||||
for name in self.conf['packages'].copy():
|
|
||||||
for d in self.conf['packages'][name]['depends']:
|
|
||||||
deps.setdefault(d, []).append(name)
|
|
||||||
return deps
|
|
||||||
|
|
||||||
def has_update(self, app):
|
|
||||||
if not self.online_packages:
|
|
||||||
return False
|
|
||||||
return parse_version(self.conf['packages'][app]['version']) < parse_version(self.online_packages[app]['version'])
|
|
@ -112,53 +112,3 @@ ISSUE = '''
|
|||||||
- \x1b[1m{url}\x1b[0m
|
- \x1b[1m{url}\x1b[0m
|
||||||
- \x1b[1m{ip}\x1b[0m\x1b[?1c
|
- \x1b[1m{ip}\x1b[0m\x1b[?1c
|
||||||
'''
|
'''
|
||||||
|
|
||||||
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}
|
|
||||||
|
|
||||||
# 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/vmmgr prepare-container {layers}
|
|
||||||
lxc.hook.post-stop = /usr/bin/vmmgr cleanup-container
|
|
||||||
|
|
||||||
# Other
|
|
||||||
lxc.arch = linux64
|
|
||||||
lxc.cap.drop = sys_admin
|
|
||||||
lxc.include = /usr/share/lxc/config/common.conf
|
|
||||||
lxc.include = /usr/share/lxc/config/userns.conf
|
|
||||||
'''
|
|
||||||
|
Loading…
Reference in New Issue
Block a user