Introduce spotter-appmgr for application, domain and port management

This commit is contained in:
Disassembler 2018-03-25 22:47:19 +02:00
parent 258ece68f5
commit dd5301a10d
Signed by: Disassembler
GPG Key ID: 524BD33A0EE29499
11 changed files with 223 additions and 108 deletions

View File

@ -80,9 +80,9 @@ chroot /mnt update-extlinux
chroot /mnt setup-timezone -z Europe/Prague chroot /mnt setup-timezone -z Europe/Prague
# Set hostname # Set hostname
echo 'vm' >/mnt/etc/hostname echo 'spottervm' >/mnt/etc/hostname
sed -i 's/localhost/vm/' /mnt/etc/network/interfaces echo '127.0.0.1 spottervm localhost localhost.localdomain' >/mnt/etc/hosts
sed -i 's/localhost /vm localhost /' /mnt/etc/hosts sed -i '/hostname/d' /mnt/etc/network/interfaces
# Enable services on boot # Enable services on boot
ln -s /etc/init.d/networking /mnt/etc/runlevels/boot ln -s /etc/init.d/networking /mnt/etc/runlevels/boot

View File

@ -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/extlinux.conf /boot/extlinux.conf
cp ${SOURCE_DIR}/boot/spotter.txt /boot/spotter.txt cp ${SOURCE_DIR}/boot/spotter.txt /boot/spotter.txt
cp ${SOURCE_DIR}/etc/inittab /etc/inittab cp ${SOURCE_DIR}/etc/inittab /etc/inittab
>/etc/motd
# Enable support for Czech characters # Enable support for Czech characters
cp ${SOURCE_DIR}/etc/rc.conf /etc/rc.conf cp ${SOURCE_DIR}/etc/rc.conf /etc/rc.conf
cp ${SOURCE_DIR}/etc/conf.d/consolefont /etc/conf.d/consolefont 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 # Configure NTP client
cp ${SOURCE_DIR}/etc/conf.d/ntpd /etc/conf.d/ntpd cp ${SOURCE_DIR}/etc/conf.d/ntpd /etc/conf.d/ntpd
# Create a self-signed certificate # Create a self-signed certificate
mkdir /etc/ssl/private 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 chmod 640 /etc/ssl/private/services.key
# Configure nginx # Configure nginx
mkdir /etc/nginx/apps
cp ${SOURCE_DIR}/etc/nginx/nginx.conf /etc/nginx/nginx.conf 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 # Copy Spotter resources
cp ${SOURCE_DIR}/usr/local/bin/portal-app-manager /usr/local/bin/portal-app-manager 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 cp -r ${SOURCE_DIR}/srv/portal /srv/portal
# Configure services # Configure services
@ -53,3 +49,6 @@ done
cp ${SOURCE_DIR}/etc/init.d/docker /etc/init.d/docker cp ${SOURCE_DIR}/etc/init.d/docker /etc/init.d/docker
rc-update add docker rc-update add docker
service docker start service docker start
# Set dummy domain and generate related files
spotter-appmgr update-domain spotter.vm 443

View File

@ -5,7 +5,6 @@
::wait:/sbin/openrc default >/dev/null 2>&1 ::wait:/sbin/openrc default >/dev/null 2>&1
# Set up getty # Set up getty
::wait:/sbin/issue-gen
tty1::respawn:/sbin/getty -l /sbin/nologin 38400 tty1 tty1::respawn:/sbin/getty -l /sbin/nologin 38400 tty1
# Stuff to do for the 3-finger salute # Stuff to do for the 3-finger salute

View File

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


View File

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

View File

@ -0,0 +1 @@
{"_": {"domain": "spotter.vm", "port": "443"}, "cluster-spotter": {}}

View File

@ -1,4 +0,0 @@
#!/bin/sh
export URL=$(ip route get 1 | awk '{print $NF;exit}')
envsubst </etc/issue.template >/etc/issue

View File

@ -1 +0,0 @@
{"cluster-spotter": {}}

View File

@ -1,11 +1,12 @@
$(function() { $(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) { $.each(data, function(id, props) {
var div = $('#'+id).show(); var div = $('#'+id).show();
if (props.hasOwnProperty('url')) 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) { $.each(props, function(key, value) {
div.find('.'+key).text(value.replace('{host}', window.location.hostname)); div.find('.'+key).text(value.replace('{host}', host));
}); });
}); });
}); });

View File

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

View File

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