Drop files reworked in SPOC
This commit is contained in:
parent
a1ceeb1ead
commit
a28bbae4d0
@ -1,5 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWJXH4Qm0kt2L86sntQH+C1zOJNQ0qMRt
|
|
||||||
0vx4krTxRs9HQTQYAy//JC92ea2aKleA8OL0JF90b1NYXcQCWdAS+vE/ng9IEAii
|
|
||||||
8C2+5nfuFeZ5YUjbQhfFblwHSM0c7hEG
|
|
||||||
-----END PUBLIC KEY-----
|
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"url": "https://repo.spotter.cz/lxc",
|
|
||||||
"user": "",
|
|
||||||
"pwd": ""
|
|
||||||
}
|
|
@ -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()
|
|
133
usr/bin/lxcmgr
133
usr/bin/lxcmgr
@ -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()
|
|
@ -1 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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'])
|
|
@ -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)])
|
|
@ -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}
|
|
||||||
}}
|
|
||||||
"""
|
|
Loading…
Reference in New Issue
Block a user