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()
|
||||
conf = Config()
|
||||
mgr = VMMgr(conf)
|
||||
vmmgr = VMMgr(conf)
|
||||
lxcmgr = LXCMgr(conf)
|
||||
if args.action == 'register-app':
|
||||
# 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':
|
||||
# Used on VM startup
|
||||
mgr.rebuild_issue()
|
||||
vmmgr.rebuild_issue()
|
||||
elif args.action == 'prepare-container':
|
||||
# Used with LXC hooks
|
||||
mgr.prepare_container()
|
||||
lxcmgr.prepare_container()
|
||||
elif args.action == 'register-container':
|
||||
# Used with LXC hooks
|
||||
mgr.register_container()
|
||||
lxcmgr.register_container()
|
||||
elif args.action == 'unregister-container':
|
||||
# Used with LXC hooks
|
||||
mgr.unregister_container()
|
||||
lxcmgr.unregister_container()
|
||||
elif args.action == 'register-proxy':
|
||||
# Used in init scripts
|
||||
mgr.register_proxy(args.app)
|
||||
lxcmgr.register_proxy(args.app)
|
||||
elif args.action == 'unregister-proxy':
|
||||
# Used in init scripts
|
||||
mgr.unregister_proxy(args.app)
|
||||
lxcmgr.unregister_proxy(args.app)
|
||||
|
@ -2,12 +2,16 @@
|
||||
|
||||
from .appmgr import AppMgr
|
||||
from .config import Config
|
||||
from .lxcmgr import LXCMgr
|
||||
from .pkgmgr import PkgMgr
|
||||
from .vmmgr import VMMgr
|
||||
from .wsgiapp import WSGIApp
|
||||
|
||||
__all__ = [
|
||||
'AppMgr',
|
||||
'Config',
|
||||
'LXCMgr',
|
||||
'PkgMgr',
|
||||
'VMMgr',
|
||||
'WSGIApp'
|
||||
]
|
||||
|
@ -1,50 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
from . import crypto
|
||||
from .pkgmgr import InstallItem, PkgMgr
|
||||
|
||||
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:
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.online_packages = {}
|
||||
|
||||
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
|
||||
self.pkgmgr = PkgMgr(conf)
|
||||
|
||||
def start_app(self, item):
|
||||
# Start the actual app service
|
||||
@ -90,24 +56,8 @@ class AppMgr:
|
||||
|
||||
def install_app(self, item):
|
||||
# Main installation function. Wrapper for download, registration and install script
|
||||
app = item.key
|
||||
# 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.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)
|
||||
item.data = InstallItem()
|
||||
self.pkgmgr.install_app(item.key, item.data)
|
||||
|
||||
def uninstall_app(self, item):
|
||||
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
|
||||
@ -115,125 +65,9 @@ class AppMgr:
|
||||
self.stop_app(item)
|
||||
if self.is_service_autostarted(app):
|
||||
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']:
|
||||
del self.conf['apps'][name]
|
||||
del self.conf['packages'][name]
|
||||
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
|
||||
self.pkgmgr.uninstall_app(app)
|
||||
|
||||
def get_services_deps(self):
|
||||
# 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 -*-
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@ -9,8 +8,6 @@ from . import crypto
|
||||
from . import templates
|
||||
from . import net
|
||||
|
||||
VERSION = '0.0.1'
|
||||
|
||||
ISSUE_FILE = '/etc/issue'
|
||||
NGINX_DIR = '/etc/nginx/conf.d'
|
||||
ACME_CRON = '/etc/periodic/daily/acme-sh'
|
||||
@ -23,12 +20,11 @@ class VMMgr:
|
||||
self.port = conf['host']['port']
|
||||
|
||||
def register_app(self, app, login, password):
|
||||
# Write a file with credentials of a newly installed application which
|
||||
# will be picked up by thread performing the installation after the install script finishes
|
||||
login = login if login else 'N/A'
|
||||
password = password if password else 'N/A'
|
||||
with open('/tmp/{}.credentials'.format(app), 'w') as f:
|
||||
f.write('{}\n{}'.format(login, password))
|
||||
# Register newly installed application and its credentials
|
||||
self.conf['apps'][app] = {'login': login if login else 'N/A',
|
||||
'password': password if password else 'N/A',
|
||||
'visible': False}
|
||||
self.conf.save()
|
||||
|
||||
def update_host(self, domain, port):
|
||||
# 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)
|
||||
# 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