Split LXC and package logic to separate modules

This commit is contained in:
Disassembler 2018-12-20 14:56:37 +01:00
parent be5e95d5c0
commit 6b306390b3
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
6 changed files with 301 additions and 271 deletions

View File

@ -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)

View File

@ -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'
] ]

View File

@ -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]}

View 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()

View 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

View File

@ -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()