319 lines
13 KiB
Python
Executable File
319 lines
13 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
CONF_FILE = '/srv/config.json'
|
|
DISCARD_IP = '[100::1]'
|
|
ISSUE_FILE = '/etc/issue'
|
|
NGINX_DIR = '/etc/nginx/conf.d'
|
|
|
|
NGINX_TEMPLATE = '''server {{
|
|
listen [::]:{port} ssl http2;
|
|
server_name {host}.{domain};
|
|
|
|
access_log /var/log/nginx/{app}.access.log;
|
|
error_log /var/log/nginx/{app}.error.log;
|
|
|
|
location / {{
|
|
proxy_pass http://{ip}:8080;
|
|
}}
|
|
|
|
error_page 502 /error.html;
|
|
location /error.html {{
|
|
root /srv/portal;
|
|
}}
|
|
}}
|
|
'''
|
|
|
|
NGINX_DEFAULT_TEMPLATE = '''server {{
|
|
listen [::]:80 default_server ipv6only=off;
|
|
|
|
location / {{
|
|
return 301 https://$host:{port}$request_uri;
|
|
}}
|
|
location /.well-known/acme-challenge/ {{
|
|
root /etc/acme.sh.d;
|
|
}}
|
|
}}
|
|
|
|
server {{
|
|
listen [::]:{port} ssl http2 default_server ipv6only=off;
|
|
root /srv/portal;
|
|
index index.html;
|
|
|
|
location / {{
|
|
try_files $uri $uri/ =404;
|
|
}}
|
|
location /config.json {{
|
|
alias /srv/config.json;
|
|
}}
|
|
}}
|
|
'''
|
|
|
|
ISSUE_TEMPLATE = '''
|
|
\x1b[1;32m _____ _ _ __ ____ __
|
|
/ ____| | | | | \\\\ \\\\ / / \\\\/ |
|
|
| (___ _ __ ___ | |_| |_ ___ _ _\\\\ \\\\ / /| \\\\ / |
|
|
\\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| |
|
|
____) | |_) | (_) | |_| || __/ | \\\\ / | | | |
|
|
|_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_| \\\\/ |_| |_|
|
|
| |
|
|
|_|\x1b[0m
|
|
|
|
|
|
|
|
|
|
\x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán.
|
|
Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení.
|
|
Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít
|
|
za následek občanské nebo trestní sankce.
|
|
|
|
|
|
\x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited.
|
|
You must have explicit, authorized permission to access or configure this
|
|
device. Unauthorized attempts and actions to access or use this system may
|
|
result in civil or criminal penalties.
|
|
|
|
|
|
|
|
|
|
Pro přístup k aplikacím otevřete URL \x1b[1mhttps://{host}\x1b[0m ve Vašem
|
|
internetovém prohlížeči.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\x1b[0;30m
|
|
'''
|
|
|
|
class SpotterManager:
|
|
def __init__(self):
|
|
# Load JSON configuration
|
|
with open(CONF_FILE, 'r') as f:
|
|
self.conf = json.load(f)
|
|
self.domain = self.conf['host']['domain']
|
|
self.port = self.conf['host']['port']
|
|
|
|
def save_conf(self):
|
|
# Save a sorted JSON configuration object with indentation
|
|
with open(CONF_FILE, 'w') as f:
|
|
json.dump(self.conf, f, sort_keys=True, indent=4)
|
|
|
|
def update_login(self, app, login, password):
|
|
# Update login and password for an app in the configuration
|
|
if login is not None:
|
|
self.conf['apps'][app]['login'] = login
|
|
if password is not None:
|
|
self.conf['apps'][app]['password'] = password
|
|
self.save_conf()
|
|
|
|
def show_tiles(self, app):
|
|
# Update tiles-shown for the app in the configuration
|
|
self.conf['apps'][app]['tiles-shown'] = True
|
|
self.save_conf()
|
|
|
|
def hide_tiles(self, app):
|
|
# Update tiles-shown for the app in the configuration
|
|
self.conf['apps'][app]['tiles-shown'] = False
|
|
self.save_conf()
|
|
|
|
def start_app(self, app):
|
|
# Start the actual app service
|
|
subprocess.call(['/sbin/service', app, 'start'])
|
|
|
|
def stop_app(self, app):
|
|
# Stop the actual app service
|
|
subprocess.call(['/sbin/service', app, 'stop'])
|
|
# Stop the app service's dependencies if they are not used by another running app
|
|
deps = self.build_deps_tree()
|
|
for dep in self.get_app_deps(app):
|
|
if not any([self.is_app_started(d) for d in deps[dep]]):
|
|
subprocess.call(['/sbin/service', dep, 'stop'])
|
|
|
|
def build_deps_tree(self):
|
|
# Fisrt, build a dictionary of {app: [needs]}
|
|
needs = {}
|
|
for app in self.conf['apps']:
|
|
needs[app] = self.get_app_deps(app)
|
|
# Then reverse it to {need: [apps]}
|
|
deps = {}
|
|
for app, need in needs.iteritems():
|
|
for n in need:
|
|
deps.setdefault(n, []).append(app)
|
|
return deps
|
|
|
|
def get_app_deps(self, app):
|
|
# Get "needs" line from init script and split it to list, skipping first two elements (docker, net)
|
|
try:
|
|
with open(os.path.join('/etc/init.d', app), 'r') as f:
|
|
return [l.split()[2:] for l in f.readlines() if l.startswith('\tneed')][0]
|
|
except:
|
|
return []
|
|
|
|
def is_app_started(self, app):
|
|
# Check OpenRC service status without calling any binary
|
|
return os.path.exists(os.path.join('/run/openrc/started', app))
|
|
|
|
def enable_autostart(self, app):
|
|
# Add the app to OpenRC default runlevel
|
|
subprocess.call(['/sbin/rc-update', 'add', app])
|
|
|
|
def disable_autostart(self, app):
|
|
# Remove the app from OpenRC default runlevel
|
|
subprocess.call(['/sbin/rc-update', 'del', app])
|
|
|
|
def register_proxy(self, app):
|
|
# Rebuild nginx configuration using IP of referenced app container and reload nginx
|
|
self.update_proxy_conf(app, self.get_container_ip(app))
|
|
subprocess.call(['/sbin/service', 'nginx', 'reload'])
|
|
|
|
def update_proxy_conf(self, app, ip):
|
|
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
|
|
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], ip=ip, domain=self.domain, port=self.port))
|
|
|
|
def unregister_proxy(self, app):
|
|
# Remove nginx configuration to prevent proxy mismatch when the container IP is reassigned to another container
|
|
self.update_proxy_conf(app, DISCARD_IP)
|
|
subprocess.call(['/sbin/service', 'nginx', 'reload'])
|
|
|
|
def get_container_ip(self, app):
|
|
# Return an IP address of a container. If the container is not running, return address from IPv6 discard prefix instead
|
|
try:
|
|
return subprocess.check_output(['/usr/bin/docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', app]).strip()
|
|
except:
|
|
return DISCARD_IP
|
|
|
|
def update_domain(self, domain, port):
|
|
self.domain = self.conf['host']['domain'] = domain
|
|
self.port = self.conf['host']['port'] = port
|
|
self.save_conf()
|
|
self.rebuild_nginx()
|
|
self.rebuild_issue()
|
|
self.restart_apps()
|
|
|
|
def rebuild_nginx(self):
|
|
# Rebuild nginx config for the portal app
|
|
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
|
|
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
|
|
# Unregister nginx proxy for apps (will be repopulated on app restart)
|
|
for app in self.conf['apps']:
|
|
self.update_proxy_conf(app, DISCARD_IP)
|
|
# Restart nginx to properly bind the new listen port
|
|
subprocess.call(['/sbin/service', 'nginx', 'restart'])
|
|
|
|
def rebuild_issue(self):
|
|
# Compile the HTTPS host displayed in terminal banner
|
|
host = self.domain
|
|
# If the dummy host is used, take an IP address of a primary interface instead
|
|
if self.domain == 'spotter.vm':
|
|
host = subprocess.check_output(['/sbin/ip', 'route', 'get', '1']).split()[-1]
|
|
# Show port number only when using the non-default HTTPS port
|
|
if self.port != '443':
|
|
host += ':{}'.format(self.port)
|
|
# Rebuild the terminal banner
|
|
with open(ISSUE_FILE, 'w') as f:
|
|
f.write(ISSUE_TEMPLATE.format(host=host))
|
|
|
|
def restart_apps(self):
|
|
for app in self.conf['apps']:
|
|
# Check if a script for internal update of URL in the app exists and is executable and run it
|
|
script_path = os.path.join('/srv', app, 'update-url.sh')
|
|
if os.path.exists(script_path) and os.access(script_path, os.X_OK):
|
|
subprocess.call([script_path, '{}.{}'.format(self.conf['apps'][app]['host'], self.domain), self.port])
|
|
# If the app is currently running, restart the app service
|
|
if self.is_app_started(app):
|
|
subprocess.call(['/sbin/service', app, 'restart'])
|
|
|
|
def request_cert(self, email):
|
|
# Compile an acme.sh command for certificate requisition
|
|
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
|
|
for app in self.conf['apps']:
|
|
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
|
|
cmd += ['-w', '/etc/acme.sh.d', '--accountemail', email]
|
|
# Request the certificate. If the requisition command fails, CalledProcessError will be raised
|
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
# Install the issued certificate
|
|
subprocess.call(['/usr/bin/acme.sh', '--installcert', '-d', self.domain, '--keypath', '/etc/ssl/private/services.key', '--fullchainpath', '/etc/ssl/certs/services.pem', '--reloadcmd', 'service nginx reload'])
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='Spotter VM application manager')
|
|
subparsers = parser.add_subparsers()
|
|
|
|
parser_update_login = subparsers.add_parser('update-login', help='Updates application login')
|
|
parser_update_login.set_defaults(action='update-login')
|
|
parser_update_login.add_argument('app', help='Application name')
|
|
parser_update_login.add_argument('login', help='Administrative login')
|
|
parser_update_login.add_argument('password', help='Administrative password')
|
|
|
|
parser_show_tiles = subparsers.add_parser('show-tiles', help='Shows application tiles in Portal')
|
|
parser_show_tiles.set_defaults(action='show-tiles')
|
|
parser_show_tiles.add_argument('app', help='Application name')
|
|
|
|
parser_hide_tiles = subparsers.add_parser('hide-tiles', help='Hides application tiles in Portal')
|
|
parser_hide_tiles.set_defaults(action='hide-tiles')
|
|
parser_hide_tiles.add_argument('app', help='Application name')
|
|
|
|
parser_start_app = subparsers.add_parser('start-app', help='Start application including it\'s dependencies')
|
|
parser_start_app.set_defaults(action='start-app')
|
|
parser_start_app.add_argument('app', help='Application name')
|
|
|
|
parser_stop_app = subparsers.add_parser('stop-app', help='Stops application including it\'s dependencies if they are not used by another running application')
|
|
parser_stop_app.set_defaults(action='stop-app')
|
|
parser_stop_app.add_argument('app', help='Application name')
|
|
|
|
parser_enable_autostart = subparsers.add_parser('enable-autostart', help='Enables application autostart')
|
|
parser_enable_autostart.set_defaults(action='enable-autostart')
|
|
parser_enable_autostart.add_argument('app', help='Application name')
|
|
|
|
parser_disable_autostart = subparsers.add_parser('disable-autostart', help='Disables application autostart')
|
|
parser_disable_autostart.set_defaults(action='disable-autostart')
|
|
parser_disable_autostart.add_argument('app', help='Application name')
|
|
|
|
parser_register_proxy = subparsers.add_parser('register-proxy', help='Rebuilds nginx proxy target for an application container')
|
|
parser_register_proxy.set_defaults(action='register-proxy')
|
|
parser_register_proxy.add_argument('app', help='Application name')
|
|
|
|
parser_unregister_proxy = subparsers.add_parser('unregister-proxy', help='Removes nginx proxy target for an application container')
|
|
parser_unregister_proxy.set_defaults(action='unregister-proxy')
|
|
parser_unregister_proxy.add_argument('app', help='Application name')
|
|
|
|
parser_update_domain = subparsers.add_parser('update-domain', help='Rebuilds domain structure of VM with new domain name and new HTTPS port')
|
|
parser_update_domain.set_defaults(action='update-domain')
|
|
parser_update_domain.add_argument('domain', help='Domain name')
|
|
parser_update_domain.add_argument('port', help='HTTPS port')
|
|
|
|
parser_request_cert = subparsers.add_parser('request-cert', help='Requests and installs Let\'s Encrypt certificate for currently set domain')
|
|
parser_request_cert.set_defaults(action='request-cert')
|
|
parser_request_cert.add_argument('email', help='Email address to receive certificate notifications')
|
|
|
|
args = parser.parse_args()
|
|
sm = SpotterManager()
|
|
if args.action == 'update-login':
|
|
sm.update_login(args.app, args.login, args.password)
|
|
elif args.action == 'show-tiles':
|
|
sm.show_tiles(args.app)
|
|
elif args.action == 'hide-tiles':
|
|
sm.hide_tiles(args.app)
|
|
elif args.action == 'start-app':
|
|
sm.start_app(args.app)
|
|
elif args.action == 'stop-app':
|
|
sm.stop_app(args.app)
|
|
elif args.action == 'enable-autostart':
|
|
sm.enable_autostart(args.app)
|
|
elif args.action == 'disable-autostart':
|
|
sm.disable_autostart(args.app)
|
|
elif args.action == 'register-proxy':
|
|
sm.register_proxy(args.app)
|
|
elif args.action == 'unregister-proxy':
|
|
sm.unregister_proxy(args.app)
|
|
elif args.action == 'update-domain':
|
|
sm.update_domain(args.domain, args.port)
|
|
elif args.action == 'request-cert':
|
|
sm.request_cert(args.email)
|