198 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/python
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| import argparse
 | |
| import json
 | |
| import os
 | |
| import subprocess
 | |
| 
 | |
| CONF_FILE = '/etc/spotter/apps.json'
 | |
| 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://{ip}: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)
 | |
| 
 | |
|     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])
 | |
|             subprocess.call(['service', app, 'restart'])
 | |
| 
 | |
|     def update_proxy(self, app):
 | |
|         self.add_app_to_nginx(app)
 | |
|         subprocess.call(['service', 'nginx', 'reload'])
 | |
| 
 | |
|     def add_app_to_nginx(self, app):
 | |
|         ip = get_container_ip(app)
 | |
|         with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
 | |
|             f.write(NGINX_TEMPLATE.format(app=app, ip=ip, domain=self.domain, port=self.port))
 | |
| 
 | |
|     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()
 | |
|         subprocess.call(['service', 'nginx', 'restart'])
 | |
| 
 | |
|     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')
 | |
|     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_proxy = subparsers.add_parser('update-proxy', help='Updates nginx proxy target for an application container')
 | |
|     parser_update_proxy.set_defaults(action='update-proxy')
 | |
|     parser_update_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')
 | |
| 
 | |
|     args = parser.parse_args()
 | |
|     sm = SpotterManager()
 | |
|     if args.action == 'add-app':
 | |
|         sm.add_app(args.app, args)
 | |
|     elif args.action == 'update-proxy':
 | |
|         sm.update_proxy(args.app)
 | |
|     elif args.action == 'update-domain':
 | |
|         sm.update_domain(args.domain, args.port)
 |