#!/usr/bin/python # -*- coding: utf-8 -*- import argparse import json import os import subprocess CONF_FILE = '/etc/spotter/apps.json' HOSTS_FILE = '/etc/hosts' ISSUE_FILE = '/etc/issue' NGINX_DIR = '/etc/nginx/conf.d' NGINX_TEMPLATE = '''server {{ listen [::]:{port} ssl http2; server_name {app}.{domain}; access_log /var/log/nginx/{app}.access.log; error_log /var/log/nginx/{app}.error.log; location / {{ proxy_pass http://{app}:8080; }} }} ''' 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 /apps.json {{ alias /etc/spotter/apps.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): self.conf = {} with open(CONF_FILE, 'r') as f: self.conf = json.load(f) self.domain = self.conf["_"]["domain"] self.port = self.conf["_"]["port"] def save_conf(self): with open(CONF_FILE, 'w') as f: json.dump(self.conf, f) def add_app(self, app, args): self.add_app_to_conf(app, args) if args.url: self.update_app_conf(app) self.add_app_to_nginx(app) self.reload_nginx() def add_app_to_conf(self, app, args): self.conf[app] = {} for key in ('url', 'login', 'password'): value = getattr(args, key) if value: self.conf[app][key] = value if args.property: for key, value in args.property: self.conf[app][key] = value self.save_conf() def update_app_conf(self, app): script_path = os.path.join('/srv', app, 'update-url.sh') if os.path.exists(script_path) and os.access(script_path, os.X_OK): host = '{}.{}'.format(app, self.domain) subprocess.call([script_path, host, self.port]) def add_app_to_nginx(self, app): with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f: f.write(NGINX_TEMPLATE.format(app=app, domain=self.domain, port=self.port)) def reload_nginx(self): subprocess.call(['service', 'nginx', 'reload']) def update_hosts(self, app): with open(HOSTS_FILE, 'r') as f: lines = f.readlines() with open(HOSTS_FILE, 'w') as f: for line in lines: if not line.strip().endswith(' {}'.format(app)): f.write(line) f.write('{} {}\n'.format(get_container_ip(app), app)) def update_domain(self, domain, port): self.domain = self.conf["_"]["domain"] = domain self.port = self.conf["_"]["port"] = port self.save_conf() self.update_app_confs() self.rebuild_nginx() self.rebuild_issue() self.reload_nginx() def update_app_confs(self): for app in self.conf.iteritems(): if 'url' in app[1]: self.update_app_conf(app[0]) def rebuild_nginx(self): for f in os.listdir(NGINX_DIR): os.unlink(os.path.join(NGINX_DIR, f)) with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f: f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port)) for app in self.conf.iteritems(): if 'url' in app[1]: self.add_app_to_nginx(app[0]) def rebuild_issue(self): host = self.domain if self.port != '443': host = '{}:{}'.format(host, self.port) with open(ISSUE_FILE, 'w') as f: f.write(ISSUE_TEMPLATE.format(host=host)) def get_container_ip(app): try: return subprocess.check_output(['docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', app]).strip() except: return '127.0.0.1' if __name__ == '__main__': parser = argparse.ArgumentParser(description='Spotter VM application manager') subparsers = parser.add_subparsers() parser_add_app = subparsers.add_parser('add-app', help='Registers a new application and creates hosts and nginx definition for it') parser_add_app.set_defaults(action='add-app') parser_add_app.add_argument('app', help='Application name') parser_add_app.add_argument('url', nargs='?', help='URL to the application. Use "{host}" as a host placeholder') parser_add_app.add_argument('login', nargs='?', help='Administrative login') parser_add_app.add_argument('password', nargs='?', help='Administrative password') parser_add_app.add_argument('-p', '--property', nargs=2, action='append', help='Add arbitrary key-value to the application properties') parser_update_app = subparsers.add_parser('update-hosts', help='Updates hosts definition for application container') parser_update_app.set_defaults(action='update-hosts') parser_update_app.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') args = parser.parse_args() sm = SpotterManager() if args.action == 'add-app': sm.add_app(args.app, args) elif args.action == 'update-hosts': sm.update_hosts(args.app) elif args.action == 'update-domain': sm.update_domain(args.domain, args.port)