Reorganize vm/lxc/app functions

This commit is contained in:
Disassembler 2019-02-19 16:05:21 +01:00
parent c213b0c0d8
commit 050b26f11f
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
7 changed files with 102 additions and 119 deletions

View File

@ -2,7 +2,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import argparse import argparse
from vmmgr import Config, LXCMgr, VMMgr from vmmgr import lxcmgr
from vmmgr.config import Config
from vmmgr.vmmgr import VMMgr
parser = argparse.ArgumentParser(description='VM application manager') parser = argparse.ArgumentParser(description='VM application manager')
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
@ -38,9 +40,7 @@ parser_unregister_proxy.set_defaults(action='unregister-proxy')
parser_unregister_proxy.add_argument('app', help='Application name') parser_unregister_proxy.add_argument('app', help='Application name')
args = parser.parse_args() args = parser.parse_args()
conf = Config() vmmgr = VMMgr(Config())
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
vmmgr.register_app(args.app, args.login, args.password) vmmgr.register_app(args.app, args.login, args.password)
@ -58,7 +58,7 @@ elif args.action == 'unregister-container':
lxcmgr.unregister_container() lxcmgr.unregister_container()
elif args.action == 'register-proxy': elif args.action == 'register-proxy':
# Used in init scripts # Used in init scripts
lxcmgr.register_proxy(args.app, args.host) vmmgr.register_proxy(args.app, args.host)
elif args.action == 'unregister-proxy': elif args.action == 'unregister-proxy':
# Used in init scripts # Used in init scripts
lxcmgr.unregister_proxy(args.app) vmmgr.unregister_proxy(args.app)

View File

@ -1,15 +1 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .appmgr import AppMgr
from .config import Config
from .lxcmgr import LXCMgr
from .vmmgr import VMMgr
from .wsgiapp import WSGIApp
__all__ = [
'AppMgr',
'Config',
'LXCMgr',
'VMMgr',
'WSGIApp'
]

View File

@ -114,15 +114,3 @@ class AppMgr:
except: except:
pass pass
return [] return []
def update_common_settings(self, email, gmaps_api_key):
# Update common configuration values
self.conf['common']['email'] = email
self.conf['common']['gmaps-api-key'] = gmaps_api_key
self.conf.save()
def shutdown_vm(self):
subprocess.run(['/sbin/poweroff'])
def reboot_vm(self):
subprocess.run(['/sbin/reboot'])

View File

@ -6,92 +6,78 @@ import shutil
import subprocess import subprocess
from . import templates from . import templates
from .config import Config
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT, NGINX_DIR from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT, NGINX_DIR
class LXCMgr: def prepare_container():
def __init__(self, conf): # Extract the variables from values given via lxc.hook.pre-start hook
# Load JSON configuration app = os.environ['LXC_NAME']
self.conf = conf # Remove ephemeral layer data
clean_ephemeral_layer(app)
# Configure host and common params used in the app
configure_app(app)
def prepare_container(self): def clean_ephemeral_layer(app):
# Extract the variables from values given via lxc.hook.pre-start hook # Cleans containers ephemeral layer.
app = os.environ['LXC_NAME'] # This is done early in the container start process, so the inode of the delta0 directory must remain unchanged
# Remove ephemeral layer data layer = os.path.join(LXC_ROOT, app, 'delta0')
self.clean_ephemeral_layer(app) if os.path.exists(layer):
# Configure host and common params used in the app for item in os.scandir(layer):
self.configure_app(app) shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
def clean_ephemeral_layer(self, app): def register_container():
# Cleans containers ephemeral layer. # Extract the variables from values given via lxc.hook.start-host hook
# This is done early in the container start process, so the inode of the delta0 directory must remain unchanged app = os.environ['LXC_NAME']
layer = os.path.join(LXC_ROOT, app, 'delta0') pid = os.environ['LXC_PID']
if os.path.exists(layer): # Lease the first unused IP to the container
for item in os.scandir(layer): ip = update_hosts_lease(app, True)
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path) # 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 register_container(self): def unregister_container():
# Extract the variables from values given via lxc.hook.start-host hook # Extract the variables from values given via lxc.hook.post-stop hook
app = os.environ['LXC_NAME'] app = os.environ['LXC_NAME']
pid = os.environ['LXC_PID'] # Release the container IP
# Lease the first unused IP to the container update_hosts_lease(app, False)
ip = self.update_hosts_lease(app, True) # Remove ephemeral layer data
# Set IP in container based on PID given via lxc.hook.start-host hook clean_ephemeral_layer(app)
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): def update_hosts_lease(app, is_request):
# Extract the variables from values given via lxc.hook.post-stop hook # This is a poor man's DHCP server which uses /etc/hosts as lease database
app = os.environ['LXC_NAME'] # Leases the first unused IP from range 172.17.0.0/16
# Release the container IP # Uses file lock as interprocess mutex
self.update_hosts_lease(app, False) ip = None
# Remove ephemeral layer data with open(HOSTS_LOCK, 'w') as lock:
self.clean_ephemeral_layer(app) fcntl.lockf(lock, fcntl.LOCK_EX)
# Load all existing records
with open(HOSTS_FILE, 'r') as f:
leases = [l.strip().split(' ', 1) for l in f]
# If this call is a request for lease, find the first unassigned IP
if is_request:
used_ips = [l[0] for l in leases]
for i in range(2, 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(HOSTS_FILE, 'w') as f:
for lease in leases:
f.write('{} {}\n'.format(lease[0], lease[1]))
return ip
def update_hosts_lease(self, app, is_request): def configure_app(app):
# This is a poor man's DHCP server which uses /etc/hosts as lease database # Supply common configuration for the application. Done as part of container preparation during service startup
# Leases the first unused IP from range 172.17.0.0/16 script = os.path.join('/srv', app, 'update-conf.sh')
# Uses file lock as interprocess mutex if os.path.exists(script):
ip = None conf = Config()
with open(HOSTS_LOCK, 'w') as lock: setup_env = os.environ.copy()
fcntl.lockf(lock, fcntl.LOCK_EX) setup_env['DOMAIN'] = conf['host']['domain']
# Load all existing records setup_env['PORT'] = conf['host']['port']
with open(HOSTS_FILE, 'r') as f: setup_env['EMAIL'] = conf['common']['email']
leases = [l.strip().split(' ', 1) for l in f] setup_env['GMAPS_API_KEY'] = conf['common']['gmaps-api-key']
# If this call is a request for lease, find the first unassigned IP subprocess.run([script], env=setup_env, check=True)
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(HOSTS_FILE, '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, host):
# 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=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()

View File

@ -21,7 +21,7 @@ class VMMgr:
def register_app(self, app, login, password): def register_app(self, app, login, password):
# Register newly installed application, its metadata and credentials # Register newly installed application, its metadata and credentials
with open('/var/lib/lxcpkgs/{app}/meta'.format(app)) as f: with open('/var/lib/lxcpkgs/{}/meta'.format(app)) as f:
meta = json.load(f) meta = json.load(f)
self.conf['apps'][app] = {**meta, self.conf['apps'][app] = {**meta,
'login': login if login else 'N/A', 'login': login if login else 'N/A',
@ -34,6 +34,17 @@ class VMMgr:
except: except:
pass pass
def register_proxy(self, app, host):
# 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=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()
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
self.domain = self.conf['host']['domain'] = domain self.domain = self.conf['host']['domain'] = domain
@ -58,6 +69,12 @@ class VMMgr:
with open(ISSUE_FILE, 'w') as f: with open(ISSUE_FILE, 'w') as f:
f.write(templates.ISSUE.format(url=net.compile_url(self.domain, self.port), ip=net.compile_url(net.get_local_ip(), self.port))) f.write(templates.ISSUE.format(url=net.compile_url(self.domain, self.port), ip=net.compile_url(net.get_local_ip(), self.port)))
def update_common_settings(self, email, gmaps_api_key):
# Update common configuration values
self.conf['common']['email'] = email
self.conf['common']['gmaps-api-key'] = gmaps_api_key
self.conf.save()
def update_password(self, oldpassword, newpassword): def update_password(self, oldpassword, newpassword):
# Update LUKS password and adminpwd for WSGI application # Update LUKS password and adminpwd for WSGI application
pwinput = '{}\n{}'.format(oldpassword, newpassword).encode() pwinput = '{}\n{}'.format(oldpassword, newpassword).encode()
@ -135,3 +152,9 @@ 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 shutdown_vm(self):
subprocess.run(['/sbin/poweroff'])
def reboot_vm(self):
subprocess.run(['/sbin/reboot'])

View File

@ -308,7 +308,7 @@ class WSGIApp:
if not validator.is_valid_email(email): if not validator.is_valid_email(email):
request.session['msg'] = 'common:error:{}'.format(request.session.lang.invalid_email(email)) request.session['msg'] = 'common:error:{}'.format(request.session.lang.invalid_email(email))
else: else:
self.appmgr.update_common_settings(email, request.form['gmaps-api-key']) self.vmmgr.update_common_settings(email, request.form['gmaps-api-key'])
request.session['msg'] = 'common:info:{}'.format(request.session.lang.common_updated()) request.session['msg'] = 'common:info:{}'.format(request.session.lang.common_updated())
return redirect('/setup-apps') return redirect('/setup-apps')
@ -380,13 +380,13 @@ class WSGIApp:
def reboot_vm_action(self, request): def reboot_vm_action(self, request):
# Reboots VM # Reboots VM
response = self.render_json({'ok': request.session.lang.reboot_initiated()}) response = self.render_json({'ok': request.session.lang.reboot_initiated()})
response.call_on_close(self.appmgr.reboot_vm) response.call_on_close(self.vmmgr.reboot_vm)
return response return response
def shutdown_vm_action(self, request): def shutdown_vm_action(self, request):
# Shuts down VM # Shuts down VM
response = self.render_json({'ok': request.session.lang.shutdown_initiated()}) response = self.render_json({'ok': request.session.lang.shutdown_initiated()})
response.call_on_close(self.appmgr.shutdown_vm) response.call_on_close(self.vmmgr.shutdown_vm)
return response return response
def reload_config_action(self, request): def reload_config_action(self, request):

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from vmmgr import WSGIApp from vmmgr.wsgiapp import WSGIApp
application = WSGIApp() application = WSGIApp()