Separate repo conf, build should now be almost complete

This commit is contained in:
Disassembler 2019-09-20 15:41:56 +02:00
parent c3b711850e
commit 4c2616887f
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
10 changed files with 87 additions and 74 deletions

5
etc/lxcmgr/repo.json Normal file
View File

@ -0,0 +1,5 @@
{
"url": "https://repo.spotter.cz/lxc",
"user": "",
"pwd": ""
}

View File

@ -8,11 +8,5 @@
"adminpwd": "${ADMINPWD}", "adminpwd": "${ADMINPWD}",
"domain": "spotter.vm", "domain": "spotter.vm",
"port": "443" "port": "443"
},
"packages": {},
"repo": {
"pwd": "",
"url": "https://repo.spotter.cz/lxc",
"user": ""
} }
} }

View File

@ -44,16 +44,6 @@ parser_container_cleanup.set_defaults(action='container-cleanup')
parser_container_cleanup.add_argument('container', help='Container name') parser_container_cleanup.add_argument('container', help='Container name')
parser_container_cleanup.add_argument('lxc', nargs=argparse.REMAINDER) 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): def print_apps(packages):
for app, meta in packages.items(): for app, meta in packages.items():
print('{} {}'.format(app, meta['version'])) print('{} {}'.format(app, meta['version']))
@ -110,9 +100,3 @@ elif args.action == 'container-prepare':
elif args.action == 'container-cleanup': elif args.action == 'container-cleanup':
# Used with LXC hooks on container stop # Used with LXC hooks on container stop
lxcmgr.cleanup_container(args.container) 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)

View File

@ -9,20 +9,20 @@ from cryptography.hazmat.primitives.asymmetric import ec
from .paths import REPO_SIG_FILE from .paths import REPO_SIG_FILE
def verify_signature(file, signature): def verify_signature(public_key_path, input_data, signature_data):
# Verifies ECDSA HMAC SHA512 signature of a file # Verifies ECDSA HMAC SHA512 signature of a file
with open(REPO_SIG_FILE, 'rb') as f: with open(public_key_path, 'rb') as f:
pub_key = serialization.load_pem_public_key(f.read(), default_backend()) pub_key = serialization.load_pem_public_key(f.read(), default_backend())
pub_key.verify(signature, file, ec.ECDSA(hashes.SHA512())) pub_key.verify(signature_data, input_data, ec.ECDSA(hashes.SHA512()))
def verify_hash(file, expected_hash): def verify_hash(input_path, expected_hash):
# Verifies SHA512 hash of a file against expected hash # Verifies SHA512 hash of a file against expected hash
sha512 = hashlib.sha512() sha512 = hashlib.sha512()
with open(file, 'rb') as f: with open(input_path, 'rb') as f:
while True: while True:
data = f.read(65536) data = f.read(65536)
if not data: if not data:
break break
sha512.update(data) sha512.update(data)
if sha512.hexdigest() != expected_hash: if sha512.hexdigest() != expected_hash:
raise InvalidSignature(file) raise InvalidSignature(input_path)

View File

@ -6,7 +6,7 @@ import shutil
import subprocess import subprocess
from . import flock from . import flock
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_LOGS, LXC_ROOT, LXC_STORAGE_DIR
from .templates import LXC_CONTAINER from .templates import LXC_CONTAINER
def prepare_container(container, layers): def prepare_container(container, layers):
@ -67,6 +67,7 @@ def create_container(container, image):
def destroy_container(container): def destroy_container(container):
# Remove container configuration and directories # Remove container configuration and directories
shutil.rmtree(os.path.join(LXC_ROOT, container)) shutil.rmtree(os.path.join(LXC_ROOT, container))
os.unlink(os.path.join(LXC_LOGS, '{}.log'.format(container)))
# Release the IP address # Release the IP address
update_hosts_lease(container, False) update_hosts_lease(container, False)

View File

@ -2,12 +2,14 @@
# Package manager # Package manager
REPO_CACHE_DIR = '/var/lib/lxcmgr/cache' REPO_CACHE_DIR = '/var/lib/lxcmgr/cache'
REPO_CONF_FILE = '/etc/lxcmgr/repo.json'
REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages' REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages'
REPO_LOCK = '/var/lock/lxcmgr-repo.lock' REPO_LOCK = '/var/lock/lxcmgr-repo.lock'
REPO_SIG_FILE = '/var/lib/lxcmgr/packages.pub' REPO_SIG_FILE = '/etc/lxcmgr/packages.pub'
# LXC # LXC
HOSTS_FILE = '/etc/hosts' HOSTS_FILE = '/etc/hosts'
HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock' HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock'
LXC_LOGS = '/var/log/lxc'
LXC_ROOT = '/var/lib/lxc' LXC_ROOT = '/var/lib/lxc'
LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage' LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage'

View File

@ -12,7 +12,7 @@ from pkg_resources import parse_version
from . import crypto from . import crypto
from . import flock from . import flock
from . import lxcmgr from . import lxcmgr
from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_LOCAL_FILE, REPO_LOCK from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_CONF_FILE, REPO_LOCAL_FILE, REPO_LOCK, REPO_SIG_FILE
class Stage(Enum): class Stage(Enum):
QUEUED = 1 QUEUED = 1
@ -31,7 +31,7 @@ class RepoFileNotFound(Exception):
class RepoBadRequest(Exception): class RepoBadRequest(Exception):
pass pass
class AppInstall: class App:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.stage = Stage.QUEUED self.stage = Stage.QUEUED
@ -44,22 +44,30 @@ class AppInstall:
return min(99, round(self.bytes_downloaded / self.bytes_total * 100)) return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
class PkgMgr: class PkgMgr:
def __init__(self, repo_url, repo_auth=None): def __init__(self):
self.repo_url = repo_url self.repo_url = None
self.repo_auth = repo_auth self.repo_auth = None
self.installed_packages = None
self.online_packages = None self.online_packages = None
def load_installed_packages(self):
with open(REPO_LOCAL_FILE, 'r') as f: with open(REPO_LOCAL_FILE, 'r') as f:
self.installed_packages = json.load(f) self.installed_packages = json.load(f)
def save_installed_packages(self, packages): def save_installed_packages(self):
with open(REPO_LOCAL_FILE, 'w') as f: with open(REPO_LOCAL_FILE, 'w') as f:
json.dump(packages, f, sort_keys=True, indent=4) 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): def get_repo_resource(self, resource_url, stream=False):
# Download requested repository resource # 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) r = requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream)
if r.status_code == 401: if r.status_code == 401:
raise RepoUnauthorized(r.url) raise RepoUnauthorized(r.url)
@ -73,65 +81,65 @@ class PkgMgr:
# Fetches and verifies online packages. Can raise InvalidSignature # Fetches and verifies online packages. Can raise InvalidSignature
packages = self.get_repo_resource('packages').content packages = self.get_repo_resource('packages').content
packages_sig = self.get_repo_resource('packages.sig').content packages_sig = self.get_repo_resource('packages.sig').content
crypto.verify_signature(packages, packages_sig) crypto.verify_signature(REPO_SIG_FILE, packages, packages_sig)
self.online_packages = json.loads(packages) self.online_packages = json.loads(packages)
@flock.flock_ex(REPO_LOCK) @flock.flock_ex(REPO_LOCK)
def install_app(self, app): def install_app(self, app):
# Main installation function. Wrapper for download, registration and install script # 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']: if app.name in self.installed_packages['apps']:
app.stage = Stage.DONE app.stage = Stage.DONE
return return
if not self.online_packages: if not self.online_packages:
self.fetch_online_packages() self.fetch_online_packages()
# Get all packages on which the app depends and which have not been installed yet # Get all packages on which the app depends and which have not been installed yet
#TODO: flatten and change name to "images"
layers = [] layers = []
images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()] images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()]
for image in images: for image in images:
layers.extend(self.online_packages['images'][image]['layers']) layers.extend(self.online_packages['images'][image]['layers'])
layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']] layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']]
# Calculate bytes to download # Calculate bytes to download
app.bytes_total = sum(self.online_packages['images'][layer]['size'] for layer in layers) + self.online_packages['apps'][app.name]['size'] app.bytes_total = sum(self.online_packages['images'][image]['size'] for image in layers) + self.online_packages['apps'][app.name]['size']
# Download layers and setup script files # Download layers and setup script files
app.stage = Stage.DOWNLOAD app.stage = Stage.DOWNLOAD
for layer in layers: for image in layers:
self.download_layer(app, layer) self.download_image(app, image)
self.download_scripts(app) self.download_scripts(app)
# Purge old data (to clean previous failed installation) and unpack # Purge old data to clean previous failed installation and unpack downloaded archives
app.stage = Stage.UNPACK app.stage = Stage.UNPACK
for layer in layers: for image in layers:
self.purge_layer(layer) self.purge_image(image)
self.unpack_layer(layer) self.unpack_image(image)
self.register_image(image, self.online_packages['images'][image])
self.purge_scripts(app.name) self.purge_scripts(app.name)
self.unpack_scripts(app.name) self.unpack_scripts(app.name)
# Run setup scripts # Run setup scripts
app.stage = Stage.INSTALL app.stage = Stage.INSTALL
# Run uninstall script to clean previous failed installation
self.run_uninstall_script(app.name) self.run_uninstall_script(app.name)
# Build containers and services # Build containers and services
self.create_containers(app.name) self.create_containers(app.name)
# Run install script and finish the installation # TODO: Create services
# Run install script and register the app
self.run_install_script(app.name) self.run_install_script(app.name)
self.installed_packages['apps'][app.name] = self.online_packages['apps'][app.name] self.register_app(app.name, self.online_packages['apps'][app.name])
self.save_installed_packages()
app.stage = Stage.DONE app.stage = Stage.DONE
def download_layer(self, app, layer): def download_image(self, app, image):
pkg_archive = 'images/{}.tar.xz'.format(layer) # Download image archive and verify hash
pkg_archive = 'images/{}.tar.xz'.format(image)
self.download_archive(app, pkg_archive) self.download_archive(app, pkg_archive)
# Verify hash crypto.verify_hash(tmp_archive, self.online_packages['images'][image]['sha512'])
crypto.verify_hash(tmp_archive, self.online_packages['images'][layer]['sha512'])
def download_scripts(self, app): def download_scripts(self, app):
# Download scripts archive and verify hash
pkg_archive = 'apps/{}.tar.xz'.format(app.name) pkg_archive = 'apps/{}.tar.xz'.format(app.name)
self.download_archive(app, pkg_archive) self.download_archive(app, pkg_archive)
# Verify hash
crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512']) crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512'])
def download_archive(self, app, archive): def download_archive(self, app, archive):
# Download the archive # Download the archive from online repository
tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive) tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive)
res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True) res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True)
with open(tmp_archive, 'wb') as f: with open(tmp_archive, 'wb') as f:
@ -139,26 +147,30 @@ class PkgMgr:
if chunk: if chunk:
app.bytes_downloaded += f.write(chunk) app.bytes_downloaded += f.write(chunk)
def purge_layer(self, layer): def purge_image(self, image):
# Delete layer files from storage directory # Delete layer files from storage directory
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, layer)) shutil.rmtree(os.path.join(LXC_STORAGE_DIR, image))
if layer in self.installed_packages['images']:
del self.installed_packages['images'][layer]
self.save_installed_packages()
def unpack_layer(self, layer): def unpack_image(self, image):
# Unpack layer archive # Unpack layer archive
tmp_archive = os.path.join(REPO_CACHE_DIR, 'images/{}.tar.xz'.format(layer)) 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) subprocess.run(['tar', 'xJf', tmp_archive], cwd=LXC_STORAGE_DIR, check=True)
os.unlink(tmp_archive) os.unlink(tmp_archive)
self.installed_packages['images'][layer] = self.online_packages['images'][layer]
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() self.save_installed_packages()
def purge_scripts(self, app): def purge_scripts(self, app):
# Delete application setup scripts from storage directory # Delete application setup scripts from storage directory
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app)) 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): def unpack_scripts(self, app):
# Unpack setup scripts archive # Unpack setup scripts archive
@ -180,6 +192,17 @@ class PkgMgr:
if os.path.exists(script_path): if os.path.exists(script_path):
subprocess.run(script_path, check=True) subprocess.run(script_path, 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, app): def create_containers(self, app):
# Create LXC containers from image and app metadata # Create LXC containers from image and app metadata
for container in self.online_packages['apps'][app]['containers']: for container in self.online_packages['apps'][app]['containers']:
@ -188,10 +211,13 @@ class PkgMgr:
if 'mounts' in self.online_packages['apps'][app]['containers'][container]: if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts'] image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
lxcmgr.create_container(container, image) lxcmgr.create_container(container, image)
# TODO: Create services
@flock.flock_ex(REPO_LOCK) @flock.flock_ex(REPO_LOCK)
def uninstall_app(self, app): def uninstall_app(self, app):
# Main uninstallation function. Wrapper for uninstall script and filesystem purge # Main uninstallation function. Wrapper for uninstall script and filesystem purge
if app not in self.installed_packages['apps']:
return
self.run_uninstall_script(app) self.run_uninstall_script(app)
self.destroy_containers(app) self.destroy_containers(app)
self.purge_scripts(app) self.purge_scripts(app)
@ -218,8 +244,8 @@ class PkgMgr:
def update_app(self, app, item): def update_app(self, app, item):
# Main update function. # Main update function.
# TODO: Implement actual update # TODO: Implement actual update
#uninstall_app(app) uninstall_app(app)
#install_app(app, item) install_app(app, item)
def has_update(self, app): def has_update(self, app):
if not self.installed_packages: if not self.installed_packages:

View File

@ -40,8 +40,8 @@ lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536 lxc.idmap = g 0 100000 65536
# Hooks # Hooks
lxc.hook.pre-start = /usr/bin/vmmgr prepare-container {layers} lxc.hook.pre-start = /usr/bin/lxcmgr container prepare {layers}
lxc.hook.post-stop = /usr/bin/vmmgr cleanup-container lxc.hook.post-stop = /usr/bin/lxcmgr container cleanup
# Other # Other
lxc.arch = linux64 lxc.arch = linux64

1
var/lib/lxcmgr/packages Normal file
View File

@ -0,0 +1 @@
{"apps": {}, "images": {}}