diff --git a/alpine.sh b/alpine.sh index 70099aa..2876397 100755 --- a/alpine.sh +++ b/alpine.sh @@ -80,9 +80,9 @@ chroot /mnt update-extlinux chroot /mnt setup-timezone -z Europe/Prague # Set hostname -echo 'vm' >/mnt/etc/hostname -sed -i 's/localhost/vm/' /mnt/etc/network/interfaces -sed -i 's/localhost /vm localhost /' /mnt/etc/hosts +echo 'spottervm' >/mnt/etc/hostname +echo '127.0.0.1 spottervm localhost localhost.localdomain' >/mnt/etc/hosts +sed -i '/hostname/d' /mnt/etc/network/interfaces # Enable services on boot ln -s /etc/init.d/networking /mnt/etc/runlevels/boot diff --git a/basic.sh b/basic.sh index b518004..f4722a9 100755 --- a/basic.sh +++ b/basic.sh @@ -16,31 +16,27 @@ cp ${SOURCE_DIR}/root/.config/htop/htoprc /root/.config/htop/htoprc cp ${SOURCE_DIR}/boot/extlinux.conf /boot/extlinux.conf cp ${SOURCE_DIR}/boot/spotter.txt /boot/spotter.txt cp ${SOURCE_DIR}/etc/inittab /etc/inittab +>/etc/motd # Enable support for Czech characters cp ${SOURCE_DIR}/etc/rc.conf /etc/rc.conf cp ${SOURCE_DIR}/etc/conf.d/consolefont /etc/conf.d/consolefont -# Set legal banner with URL -cp ${SOURCE_DIR}/etc/issue.template /etc/issue.template -cp ${SOURCE_DIR}/sbin/issue-gen /sbin/issue-gen ->/etc/motd - # Configure NTP client cp ${SOURCE_DIR}/etc/conf.d/ntpd /etc/conf.d/ntpd # Create a self-signed certificate mkdir /etc/ssl/private -openssl req -x509 -new -out /etc/ssl/certs/services.pem -keyout /etc/ssl/private/services.key -nodes -days 3654 -subj "/C=CZ/CN=$(hostname -f)" +openssl req -x509 -new -out /etc/ssl/certs/services.pem -keyout /etc/ssl/private/services.key -nodes -days 3654 -subj "/CN=$(hostname)" chmod 640 /etc/ssl/private/services.key # Configure nginx -mkdir /etc/nginx/apps cp ${SOURCE_DIR}/etc/nginx/nginx.conf /etc/nginx/nginx.conf -cp ${SOURCE_DIR}/etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf -# Copy Portal resources -cp ${SOURCE_DIR}/usr/local/bin/portal-app-manager /usr/local/bin/portal-app-manager +# Copy Spotter resources +mkdir /etc/spotter +cp ${SOURCE_DIR}/etc/spotter/apps.json /etc/spotter/apps.json +cp ${SOURCE_DIR}/usr/local/bin/spotter-appmgr /usr/local/bin/spotter-appmgr cp -r ${SOURCE_DIR}/srv/portal /srv/portal # Configure services @@ -53,3 +49,6 @@ done cp ${SOURCE_DIR}/etc/init.d/docker /etc/init.d/docker rc-update add docker service docker start + +# Set dummy domain and generate related files +spotter-appmgr update-domain spotter.vm 443 diff --git a/basic/etc/inittab b/basic/etc/inittab index de9fabf..2cb0b4d 100644 --- a/basic/etc/inittab +++ b/basic/etc/inittab @@ -5,7 +5,6 @@ ::wait:/sbin/openrc default >/dev/null 2>&1 # Set up getty -::wait:/sbin/issue-gen tty1::respawn:/sbin/getty -l /sbin/nologin 38400 tty1 # Stuff to do for the 3-finger salute diff --git a/basic/etc/issue.template b/basic/etc/issue.template deleted file mode 100644 index 6298de1..0000000 --- a/basic/etc/issue.template +++ /dev/null @@ -1,36 +0,0 @@ - - _____ _ _ __ ____ __ - / ____| | | | | \\ \\ / / \\/ | - | (___ _ __ ___ | |_| |_ ___ _ _\\ \\ / /| \\ / | - \\___ \\| '_ \\ / _ \\| __| __/ _ \\ '__\\ \\/ / | |\\/| | - ____) | |_) | (_) | |_| || __/ | \\ / | | | | - |_____/| .__/ \\___/ \\__|\\__\\___|_| \\/ |_| |_| - | | - |_| - - - - - UPOZORNĚNÍ: 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. - - - CAUTION: 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 https://${URL}/ ve Vašem - internetovém prohlížeči. - - - - - - - diff --git a/basic/etc/nginx/conf.d/default.conf b/basic/etc/nginx/conf.d/default.conf deleted file mode 100644 index 177878c..0000000 --- a/basic/etc/nginx/conf.d/default.conf +++ /dev/null @@ -1,17 +0,0 @@ -server { - listen [::]:80 default_server ipv6only=off; - return 301 https://$host$request_uri; -} - -server { - listen [::]:443 ssl http2 default_server ipv6only=off; - - add_header Strict-Transport-Security "max-age=31536000;"; - - root /srv/portal; - index index.html; - - location / { - try_files $uri $uri/ =404; - } -} diff --git a/basic/etc/spotter/apps.json b/basic/etc/spotter/apps.json new file mode 100644 index 0000000..7cfd9a6 --- /dev/null +++ b/basic/etc/spotter/apps.json @@ -0,0 +1 @@ +{"_": {"domain": "spotter.vm", "port": "443"}, "cluster-spotter": {}} diff --git a/basic/sbin/issue-gen b/basic/sbin/issue-gen deleted file mode 100755 index 981947f..0000000 --- a/basic/sbin/issue-gen +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -export URL=$(ip route get 1 | awk '{print $NF;exit}') -envsubst /etc/issue diff --git a/basic/srv/portal/js/apps.json b/basic/srv/portal/js/apps.json deleted file mode 100644 index 025a9ad..0000000 --- a/basic/srv/portal/js/apps.json +++ /dev/null @@ -1 +0,0 @@ -{"cluster-spotter": {}} \ No newline at end of file diff --git a/basic/srv/portal/js/script.js b/basic/srv/portal/js/script.js index df869f7..7a9558e 100644 --- a/basic/srv/portal/js/script.js +++ b/basic/srv/portal/js/script.js @@ -1,11 +1,12 @@ $(function() { - $.getJSON('js/apps.json', function(data) { + $.getJSON('apps.json', function(data) { + var host = data._.domain + (data._.port != '443' ? ':'+data._.port : '') $.each(data, function(id, props) { var div = $('#'+id).show(); if (props.hasOwnProperty('url')) - div.find('h2 a').attr('href', props.url.replace('{host}', window.location.hostname)); + div.find('h2 a').attr('href', props.url.replace('{host}', host)); $.each(props, function(key, value) { - div.find('.'+key).text(value.replace('{host}', window.location.hostname)); + div.find('.'+key).text(value.replace('{host}', host)); }); }); }); diff --git a/basic/usr/local/bin/portal-app-manager b/basic/usr/local/bin/portal-app-manager deleted file mode 100755 index 7c08309..0000000 --- a/basic/usr/local/bin/portal-app-manager +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python - -import argparse -import json - -def main(args): - jsonfile = '/srv/portal/js/apps.json' - - data = {} - with open(jsonfile, 'r') as f: - data = json.load(f) - if not args.app in data: - data[args.app] = {} - if args.url: - data[args.app]['url'] = args.url - if args.login: - data[args.app]['login'] = args.login - if args.password: - data[args.app]['password'] = args.password - if args.property: - for key, value in args.property: - data[args.app][key] = value - with open(jsonfile, 'w') as f: - json.dump(data, f) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Spotter Cluster portal application manager') - parser.add_argument('app', help='Application ID.') - parser.add_argument('url', nargs='?', help='URL to the application. Use "{host}" as a host placeholder.') - parser.add_argument('login', nargs='?', help='Administrative login.') - parser.add_argument('password', nargs='?', help='Administrative password.') - parser.add_argument('-p', '--property', nargs=2, action='append', help='Add arbitrary key-value to the application properties') - main(parser.parse_args()) diff --git a/basic/usr/local/bin/spotter-appmgr b/basic/usr/local/bin/spotter-appmgr new file mode 100755 index 0000000..447a522 --- /dev/null +++ b/basic/usr/local/bin/spotter-appmgr @@ -0,0 +1,206 @@ +#!/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)