Split LXC and package logic to separate modules
This commit is contained in:
parent
be5e95d5c0
commit
6b306390b3
@ -38,25 +38,26 @@ parser_unregister_proxy.add_argument('app', help='Application name')
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
conf = Config()
|
conf = Config()
|
||||||
mgr = VMMgr(conf)
|
vmmgr = VMMgr(conf)
|
||||||
|
lxcmgr = LXCMgr(conf)
|
||||||
if args.action == 'register-app':
|
if args.action == 'register-app':
|
||||||
# Used by app install scripts
|
# Used by app install scripts
|
||||||
mgr.register_app(args.app, args.login, args.password)
|
vmmgr.register_app(args.app, args.login, args.password)
|
||||||
elif args.action == 'rebuild-issue':
|
elif args.action == 'rebuild-issue':
|
||||||
# Used on VM startup
|
# Used on VM startup
|
||||||
mgr.rebuild_issue()
|
vmmgr.rebuild_issue()
|
||||||
elif args.action == 'prepare-container':
|
elif args.action == 'prepare-container':
|
||||||
# Used with LXC hooks
|
# Used with LXC hooks
|
||||||
mgr.prepare_container()
|
lxcmgr.prepare_container()
|
||||||
elif args.action == 'register-container':
|
elif args.action == 'register-container':
|
||||||
# Used with LXC hooks
|
# Used with LXC hooks
|
||||||
mgr.register_container()
|
lxcmgr.register_container()
|
||||||
elif args.action == 'unregister-container':
|
elif args.action == 'unregister-container':
|
||||||
# Used with LXC hooks
|
# Used with LXC hooks
|
||||||
mgr.unregister_container()
|
lxcmgr.unregister_container()
|
||||||
elif args.action == 'register-proxy':
|
elif args.action == 'register-proxy':
|
||||||
# Used in init scripts
|
# Used in init scripts
|
||||||
mgr.register_proxy(args.app)
|
lxcmgr.register_proxy(args.app)
|
||||||
elif args.action == 'unregister-proxy':
|
elif args.action == 'unregister-proxy':
|
||||||
# Used in init scripts
|
# Used in init scripts
|
||||||
mgr.unregister_proxy(args.app)
|
lxcmgr.unregister_proxy(args.app)
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
from .appmgr import AppMgr
|
from .appmgr import AppMgr
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
from .lxcmgr import LXCMgr
|
||||||
|
from .pkgmgr import PkgMgr
|
||||||
from .vmmgr import VMMgr
|
from .vmmgr import VMMgr
|
||||||
from .wsgiapp import WSGIApp
|
from .wsgiapp import WSGIApp
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AppMgr',
|
'AppMgr',
|
||||||
'Config',
|
'Config',
|
||||||
|
'LXCMgr',
|
||||||
|
'PkgMgr',
|
||||||
'VMMgr',
|
'VMMgr',
|
||||||
'WSGIApp'
|
'WSGIApp'
|
||||||
]
|
]
|
||||||
|
@ -1,50 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import requests
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from cryptography.exceptions import InvalidSignature
|
from .pkgmgr import InstallItem, PkgMgr
|
||||||
|
|
||||||
from . import crypto
|
|
||||||
|
|
||||||
LXC_ROOT = '/var/lib/lxc'
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
|
|
||||||
class InstallItem:
|
|
||||||
def __init__(self, total):
|
|
||||||
# Stage 0 = download, 1 = deps install, 2 = app install
|
|
||||||
self.stage = 0
|
|
||||||
self.total = total
|
|
||||||
self.downloaded = 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
# Limit the displayed percentage to 0 - 99
|
|
||||||
return str(min(99, round(self.downloaded / self.total * 100)))
|
|
||||||
|
|
||||||
class AppMgr:
|
class AppMgr:
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.online_packages = {}
|
self.pkgmgr = PkgMgr(conf)
|
||||||
|
|
||||||
def get_repo_resource(self, url, stream=False):
|
|
||||||
return requests.get('{}/{}'.format(self.conf['repo']['url'], url), auth=(self.conf['repo']['user'], self.conf['repo']['pwd']), timeout=5, stream=stream)
|
|
||||||
|
|
||||||
def fetch_online_packages(self):
|
|
||||||
# Fetches and verifies online packages. Can raise InvalidSignature
|
|
||||||
online_packages = {}
|
|
||||||
packages = self.get_repo_resource('packages')
|
|
||||||
if packages.status_code != 200:
|
|
||||||
return packages.status_code
|
|
||||||
packages = packages.content
|
|
||||||
packages_sig = self.get_repo_resource('packages.sig').content
|
|
||||||
crypto.verify_signature(packages, packages_sig)
|
|
||||||
online_packages = json.loads(packages)
|
|
||||||
# Minimze the time when self.online_packages is out of sync
|
|
||||||
self.online_packages = online_packages
|
|
||||||
return 200
|
|
||||||
|
|
||||||
def start_app(self, item):
|
def start_app(self, item):
|
||||||
# Start the actual app service
|
# Start the actual app service
|
||||||
@ -90,24 +56,8 @@ class AppMgr:
|
|||||||
|
|
||||||
def install_app(self, item):
|
def install_app(self, item):
|
||||||
# Main installation function. Wrapper for download, registration and install script
|
# Main installation function. Wrapper for download, registration and install script
|
||||||
app = item.key
|
item.data = InstallItem()
|
||||||
# Clean packages which previously failed to install
|
self.pkgmgr.install_app(item.key, item.data)
|
||||||
self.clean_pending_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'] or 'pending' in self.conf['packages'][d]]
|
|
||||||
item.data = InstallItem(sum(self.online_packages[d]['size'] for d in deps))
|
|
||||||
for dep in deps:
|
|
||||||
self.download_package(dep, item.data)
|
|
||||||
for dep in deps:
|
|
||||||
item.data.stage = 2 if dep == deps[-1] else 1
|
|
||||||
# Purge old data before unpacking to clean previous failed installation
|
|
||||||
self.purge_package(dep)
|
|
||||||
self.unpack_package(dep)
|
|
||||||
# Run uninstall script before installation to clean previous failed installation
|
|
||||||
self.run_uninstall_script(dep)
|
|
||||||
self.register_package(dep)
|
|
||||||
self.run_install_script(dep)
|
|
||||||
self.finalize_installation(dep)
|
|
||||||
|
|
||||||
def uninstall_app(self, item):
|
def uninstall_app(self, item):
|
||||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
||||||
@ -115,125 +65,9 @@ class AppMgr:
|
|||||||
self.stop_app(item)
|
self.stop_app(item)
|
||||||
if self.is_service_autostarted(app):
|
if self.is_service_autostarted(app):
|
||||||
self.update_app_autostart(app, False)
|
self.update_app_autostart(app, False)
|
||||||
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 download_package(self, name, installitem):
|
|
||||||
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
|
||||||
r = self.get_repo_resource('{}.tar.xz'.format(name), True)
|
|
||||||
with open(tmp_archive, 'wb') as f:
|
|
||||||
for chunk in r.iter_content(chunk_size=65536):
|
|
||||||
if chunk:
|
|
||||||
installitem.downloaded += f.write(chunk)
|
|
||||||
# Verify hash
|
|
||||||
if self.online_packages[name]['sha512'] != self.hash_file(tmp_archive):
|
|
||||||
raise InvalidSignature(name)
|
|
||||||
|
|
||||||
def hash_file(self, file_path):
|
|
||||||
sha512 = hashlib.sha512()
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
while True:
|
|
||||||
data = f.read(65536)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
sha512.update(data)
|
|
||||||
return sha512.hexdigest()
|
|
||||||
|
|
||||||
def unpack_package(self, name):
|
|
||||||
# Unpack archive
|
|
||||||
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
|
||||||
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 local configuration
|
|
||||||
metadata = self.online_packages[name].copy()
|
|
||||||
del metadata['sha512']
|
|
||||||
del metadata['size']
|
|
||||||
metadata['pending'] = True
|
|
||||||
self.conf['packages'][name] = metadata
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def unregister_package(self, name):
|
|
||||||
# Removes a package from local configuration
|
|
||||||
if name in self.conf['apps']:
|
if name in self.conf['apps']:
|
||||||
del self.conf['apps'][name]
|
del self.conf['apps'][name]
|
||||||
del self.conf['packages'][name]
|
self.pkgmgr.uninstall_app(app)
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def finalize_installation(self, name):
|
|
||||||
# If the install script called vmmgr register-app, perform the app registration
|
|
||||||
# This can't be done directly from install script due to possible race conditions
|
|
||||||
cred_file = '/tmp/{}.credentials'.format(name)
|
|
||||||
if os.path.exists(cred_file):
|
|
||||||
with open(cred_file, 'r') as f:
|
|
||||||
cred = f.read().splitlines()
|
|
||||||
os.unlink(cred_file)
|
|
||||||
self.conf['apps'][name] = {
|
|
||||||
'login': cred[0],
|
|
||||||
'password': cred[1],
|
|
||||||
'visible': False
|
|
||||||
}
|
|
||||||
# Finally, mark the package as fully installed
|
|
||||||
del self.conf['packages'][name]['pending']
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def clean_pending_packages(self):
|
|
||||||
# Remove registeres packages with pending flag set from previously failed installation
|
|
||||||
for name in self.conf['packages'].copy():
|
|
||||||
if 'pending' in self.conf['packages'][name]:
|
|
||||||
self.unregister_package(name)
|
|
||||||
self.conf.save()
|
|
||||||
|
|
||||||
def run_install_script(self, name):
|
|
||||||
# Runs install.sh for a package, if the script is present
|
|
||||||
install_dir = os.path.join('/srv/', name, 'install')
|
|
||||||
install_script = os.path.join('/srv/', name, 'install.sh')
|
|
||||||
if os.path.exists(install_script):
|
|
||||||
subprocess.run(install_script, check=True)
|
|
||||||
os.unlink(install_script)
|
|
||||||
if os.path.exists(install_dir):
|
|
||||||
shutil.rmtree(install_dir)
|
|
||||||
|
|
||||||
def run_uninstall_script(self, name):
|
|
||||||
# Runs uninstall.sh for a package, if the script is present
|
|
||||||
uninstall_script = os.path.join('/srv/', name, 'uninstall.sh')
|
|
||||||
if os.path.exists(uninstall_script):
|
|
||||||
subprocess.run(uninstall_script, 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]['deps'].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]['deps']:
|
|
||||||
deps.setdefault(d, []).append(name)
|
|
||||||
return deps
|
|
||||||
|
|
||||||
def get_services_deps(self):
|
def get_services_deps(self):
|
||||||
# Fisrt, build a dictionary of {app: [needs]}
|
# Fisrt, build a dictionary of {app: [needs]}
|
||||||
|
98
usr/lib/python3.6/vmmgr/lxcmgr.py
Normal file
98
usr/lib/python3.6/vmmgr/lxcmgr.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from . import templates
|
||||||
|
|
||||||
|
NGINX_DIR = '/etc/nginx/conf.d'
|
||||||
|
|
||||||
|
class LXCMgr:
|
||||||
|
def __init__(self, conf):
|
||||||
|
# Load JSON configuration
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
def prepare_container(self):
|
||||||
|
# Extract the variables from values given via lxc.hook.pre-start hook
|
||||||
|
app = os.environ['LXC_NAME']
|
||||||
|
# Remove ephemeral layer data
|
||||||
|
self.clean_ephemeral_layer(app)
|
||||||
|
# Configure host and common params used in the app
|
||||||
|
self.configure_app(app)
|
||||||
|
|
||||||
|
def clean_ephemeral_layer(self, app):
|
||||||
|
# Cleans containers ephemeral layer.
|
||||||
|
# This is done early in the container start process, so the inode of the delta0 directory must remain unchanged
|
||||||
|
layer = os.path.join('/var/lib/lxc', app, 'delta0')
|
||||||
|
if os.path.exists(layer):
|
||||||
|
for item in os.scandir(layer):
|
||||||
|
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
|
||||||
|
|
||||||
|
def register_container(self):
|
||||||
|
# Extract the variables from values given via lxc.hook.start-host hook
|
||||||
|
app = os.environ['LXC_NAME']
|
||||||
|
pid = os.environ['LXC_PID']
|
||||||
|
# Lease the first unused IP to the container
|
||||||
|
ip = self.update_hosts_lease(app, True)
|
||||||
|
# Set IP in container based on PID given via lxc.hook.start-host hook
|
||||||
|
cmd = 'ip addr add {}/16 broadcast 172.17.255.255 dev eth0 && ip route add default via 172.17.0.1'.format(ip)
|
||||||
|
subprocess.run(['nsenter', '-a', '-t', pid, '--', '/bin/sh', '-c', cmd])
|
||||||
|
|
||||||
|
def unregister_container(self):
|
||||||
|
# Extract the variables from values given via lxc.hook.post-stop hook
|
||||||
|
app = os.environ['LXC_NAME']
|
||||||
|
# Release the container IP
|
||||||
|
self.update_hosts_lease(app, False)
|
||||||
|
# Remove ephemeral layer data
|
||||||
|
self.clean_ephemeral_layer(app)
|
||||||
|
|
||||||
|
def update_hosts_lease(self, 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('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
||||||
|
fcntl.lockf(lock, fcntl.LOCK_EX)
|
||||||
|
# Load all existing records
|
||||||
|
with open('/etc/hosts', '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, 65534):
|
||||||
|
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('/etc/hosts', 'w') as f:
|
||||||
|
for lease in leases:
|
||||||
|
f.write('{} {}\n'.format(lease[0], lease[1]))
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def configure_app(self, 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):
|
||||||
|
setup_env = os.environ.copy()
|
||||||
|
setup_env['DOMAIN'] = self.conf['host']['domain']
|
||||||
|
setup_env['PORT'] = self.conf['host']['port']
|
||||||
|
setup_env['EMAIL'] = self.conf['common']['email']
|
||||||
|
setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key']
|
||||||
|
subprocess.run([script], env=setup_env, check=True)
|
||||||
|
|
||||||
|
def register_proxy(self, app):
|
||||||
|
# Setup proxy configuration and reload nginx
|
||||||
|
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
||||||
|
f.write(templates.NGINX.format(app=app, host=self.conf['packages'][app]['host'], domain=self.conf['host']['domain'], port=self.conf['host']['port']))
|
||||||
|
self.reload_nginx()
|
||||||
|
|
||||||
|
def unregister_proxy(self, app):
|
||||||
|
# Remove proxy configuration and reload nginx
|
||||||
|
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
||||||
|
self.reload_nginx()
|
180
usr/lib/python3.6/vmmgr/pkgmgr.py
Normal file
180
usr/lib/python3.6/vmmgr/pkgmgr.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
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 . import crypto
|
||||||
|
|
||||||
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
|
|
||||||
|
STAGE_DOWNLOAD = 0
|
||||||
|
STAGE_INSTALL_DEPS = 1
|
||||||
|
STAGE_INSTALL_APP = 2
|
||||||
|
|
||||||
|
class InstallItem:
|
||||||
|
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.repo_url = repo_url
|
||||||
|
self.conf = conf
|
||||||
|
self.online_packages = {}
|
||||||
|
|
||||||
|
def get_repo_resource(self, resource_url, stream=False):
|
||||||
|
return requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream)
|
||||||
|
|
||||||
|
def fetch_online_packages(self):
|
||||||
|
# Fetches and verifies online packages. Can raise InvalidSignature
|
||||||
|
packages = self.get_repo_resource('packages')
|
||||||
|
if packages.status_code != 200:
|
||||||
|
return packages.status_code
|
||||||
|
packages = packages.content
|
||||||
|
packages_sig = self.get_repo_resource('packages.sig').content
|
||||||
|
crypto.verify_signature(packages, packages_sig)
|
||||||
|
self.online_packages = json.loads(packages)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
def install_app(self, app, item):
|
||||||
|
# Main installation function. Wrapper for download, registration and install script
|
||||||
|
self.fetch_online_packages()
|
||||||
|
# Clean packages which previously failed to install
|
||||||
|
self.clean_pending_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'] or 'pending' in self.conf['packages'][d]]
|
||||||
|
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:
|
||||||
|
# Set stage to INSTALLING_DEPS or INSTALLING based on which backage in sequence is being installed
|
||||||
|
item.stage = STAGE_INSTALL_APP if dep == deps[-1] else STAGE_INSTALL_DEPS
|
||||||
|
# Purge old data before unpacking to clean previous failed installation
|
||||||
|
self.purge_package(dep)
|
||||||
|
self.unpack_package(dep)
|
||||||
|
# Run uninstall script before installation to clean previous failed installation
|
||||||
|
self.run_uninstall_script(dep)
|
||||||
|
self.register_package(dep)
|
||||||
|
self.run_install_script(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 download_package(self, name, item):
|
||||||
|
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
||||||
|
r = self.get_repo_resource('{}.tar.xz'.format(name), True)
|
||||||
|
with open(tmp_archive, 'wb') as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=65536):
|
||||||
|
if chunk:
|
||||||
|
item.bytes_downloaded += f.write(chunk)
|
||||||
|
# Verify hash
|
||||||
|
if self.online_packages[name]['sha512'] != self.hash_file(tmp_archive):
|
||||||
|
raise InvalidSignature(name)
|
||||||
|
|
||||||
|
def hash_file(self, file_path):
|
||||||
|
sha512 = hashlib.sha512()
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(65536)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha512.update(data)
|
||||||
|
return sha512.hexdigest()
|
||||||
|
|
||||||
|
def unpack_package(self, name):
|
||||||
|
# Unpack archive
|
||||||
|
tmp_archive = '/tmp/{}.tar.xz'.format(name)
|
||||||
|
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']
|
||||||
|
metadata['pending'] = True
|
||||||
|
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 clean_pending_packages(self):
|
||||||
|
# Remove registered packages with pending flag set from previously failed installation
|
||||||
|
for name in self.conf['packages'].copy():
|
||||||
|
if 'pending' in self.conf['packages'][name]:
|
||||||
|
self.unregister_package(name)
|
||||||
|
self.conf.save()
|
||||||
|
|
||||||
|
def run_install_script(self, name):
|
||||||
|
# Runs install.sh for a package, if the script is present
|
||||||
|
install_dir = os.path.join('/srv/', name, 'install')
|
||||||
|
install_script = os.path.join('/srv/', name, 'install.sh')
|
||||||
|
if os.path.exists(install_script):
|
||||||
|
subprocess.run(install_script, check=True)
|
||||||
|
os.unlink(install_script)
|
||||||
|
if os.path.exists(install_dir):
|
||||||
|
shutil.rmtree(install_dir)
|
||||||
|
# Reload config to reflect whatever vmmgr register-app from the install script has written in it
|
||||||
|
self.conf.load()
|
||||||
|
del self.conf['packages'][name]['pending']
|
||||||
|
self.conf.save()
|
||||||
|
|
||||||
|
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]['deps'].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]['deps']:
|
||||||
|
deps.setdefault(d, []).append(name)
|
||||||
|
return deps
|
@ -1,6 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import fcntl
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -9,8 +8,6 @@ from . import crypto
|
|||||||
from . import templates
|
from . import templates
|
||||||
from . import net
|
from . import net
|
||||||
|
|
||||||
VERSION = '0.0.1'
|
|
||||||
|
|
||||||
ISSUE_FILE = '/etc/issue'
|
ISSUE_FILE = '/etc/issue'
|
||||||
NGINX_DIR = '/etc/nginx/conf.d'
|
NGINX_DIR = '/etc/nginx/conf.d'
|
||||||
ACME_CRON = '/etc/periodic/daily/acme-sh'
|
ACME_CRON = '/etc/periodic/daily/acme-sh'
|
||||||
@ -23,12 +20,11 @@ class VMMgr:
|
|||||||
self.port = conf['host']['port']
|
self.port = conf['host']['port']
|
||||||
|
|
||||||
def register_app(self, app, login, password):
|
def register_app(self, app, login, password):
|
||||||
# Write a file with credentials of a newly installed application which
|
# Register newly installed application and its credentials
|
||||||
# will be picked up by thread performing the installation after the install script finishes
|
self.conf['apps'][app] = {'login': login if login else 'N/A',
|
||||||
login = login if login else 'N/A'
|
'password': password if password else 'N/A',
|
||||||
password = password if password else 'N/A'
|
'visible': False}
|
||||||
with open('/tmp/{}.credentials'.format(app), 'w') as f:
|
self.conf.save()
|
||||||
f.write('{}\n{}'.format(login, password))
|
|
||||||
|
|
||||||
def update_host(self, domain, port):
|
def update_host(self, domain, port):
|
||||||
# Update domain and port and rebuild all configuration. Web interface calls restart_nginx() in WSGI close handler
|
# Update domain and port and rebuild all configuration. Web interface calls restart_nginx() in WSGI close handler
|
||||||
@ -107,86 +103,3 @@ class VMMgr:
|
|||||||
os.chmod(crypto.CERT_KEY_FILE, 0o640)
|
os.chmod(crypto.CERT_KEY_FILE, 0o640)
|
||||||
# Reload nginx
|
# Reload nginx
|
||||||
self.reload_nginx()
|
self.reload_nginx()
|
||||||
|
|
||||||
def prepare_container(self):
|
|
||||||
# Extract the variables from values given via lxc.hook.pre-start hook
|
|
||||||
app = os.environ['LXC_NAME']
|
|
||||||
# Remove ephemeral layer data
|
|
||||||
self.clean_ephemeral_layer(app)
|
|
||||||
# Configure host and common params used in the app
|
|
||||||
self.configure_app(app)
|
|
||||||
|
|
||||||
def clean_ephemeral_layer(self, app):
|
|
||||||
# Cleans containers ephemeral layer.
|
|
||||||
# This is done early in the container start process, so the inode of the delta0 directory must remain unchanged
|
|
||||||
layer = os.path.join('/var/lib/lxc', app, 'delta0')
|
|
||||||
if os.path.exists(layer):
|
|
||||||
for item in os.scandir(layer):
|
|
||||||
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
|
|
||||||
|
|
||||||
def register_container(self):
|
|
||||||
# Extract the variables from values given via lxc.hook.start-host hook
|
|
||||||
app = os.environ['LXC_NAME']
|
|
||||||
pid = os.environ['LXC_PID']
|
|
||||||
# Lease the first unused IP to the container
|
|
||||||
ip = self.update_hosts_lease(app, True)
|
|
||||||
# Set IP in container based on PID given via lxc.hook.start-host hook
|
|
||||||
cmd = 'ip addr add {}/16 broadcast 172.17.255.255 dev eth0 && ip route add default via 172.17.0.1'.format(ip)
|
|
||||||
subprocess.run(['nsenter', '-a', '-t', pid, '--', '/bin/sh', '-c', cmd])
|
|
||||||
|
|
||||||
def unregister_container(self):
|
|
||||||
# Extract the variables from values given via lxc.hook.post-stop hook
|
|
||||||
app = os.environ['LXC_NAME']
|
|
||||||
# Release the container IP
|
|
||||||
self.update_hosts_lease(app, False)
|
|
||||||
# Remove ephemeral layer data
|
|
||||||
self.clean_ephemeral_layer(app)
|
|
||||||
|
|
||||||
def update_hosts_lease(self, 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('/var/lock/vmmgr-hosts.lock', 'w') as lock:
|
|
||||||
fcntl.lockf(lock, fcntl.LOCK_EX)
|
|
||||||
# Load all existing records
|
|
||||||
with open('/etc/hosts', '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, 65534):
|
|
||||||
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('/etc/hosts', 'w') as f:
|
|
||||||
for lease in leases:
|
|
||||||
f.write('{} {}\n'.format(lease[0], lease[1]))
|
|
||||||
return ip
|
|
||||||
|
|
||||||
def configure_app(self, 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):
|
|
||||||
setup_env = os.environ.copy()
|
|
||||||
setup_env['DOMAIN'] = self.domain
|
|
||||||
setup_env['PORT'] = self.port
|
|
||||||
setup_env['EMAIL'] = self.conf['common']['email']
|
|
||||||
setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key']
|
|
||||||
subprocess.run([script], env=setup_env, check=True)
|
|
||||||
|
|
||||||
def register_proxy(self, app):
|
|
||||||
# Setup proxy configuration and reload nginx
|
|
||||||
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
|
||||||
f.write(templates.NGINX.format(app=app, host=self.conf['packages'][app]['host'], domain=self.domain, port=self.port))
|
|
||||||
self.reload_nginx()
|
|
||||||
|
|
||||||
def unregister_proxy(self, app):
|
|
||||||
# Remove proxy configuration and reload nginx
|
|
||||||
os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app)))
|
|
||||||
self.reload_nginx()
|
|
||||||
|
Loading…
Reference in New Issue
Block a user