#!/usr/bin/env python3 # Layer index Docker setup script # # Copyright (C) 2018 Intel Corporation # Author: Amber Elliot # Copyright (C) 2023 Konsulko Group # Author: Tim Orling # # Licensed under the MIT license, see COPYING.MIT for details # # SPDX-License-Identifier: MIT # This script will make a cluster of 5 containers: # # - layersapp: the application # - layersdb: the database # - layersweb: NGINX web server (as a proxy and for serving static content) # - layerscelery: Celery (for running background jobs) # - layersrabbit: RabbitMQ (required by Celery) # # It will build and run these containers and set up the database. import sys min_version = (3, 4, 3) if sys.version_info < min_version: sys.stderr.write('Sorry, python version %d.%d.%d or later is required\n' % min_version) sys.exit(1) import os import argparse import re import subprocess import time import random import shutil import tempfile from shlex import quote def get_args(): parser = argparse.ArgumentParser(description='Script sets up the Layer Index tool with Docker Containers.') default_http_proxy = os.environ.get('http_proxy', os.environ.get('HTTP_PROXY', '')) default_https_proxy = os.environ.get('https_proxy', os.environ.get('HTTPS_PROXY', '')) default_socks_proxy = os.environ.get('socks_proxy', os.environ.get('SOCKS_PROXY', os.environ.get('all_proxy', os.environ.get('ALL_PROXY', '')))) default_no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) parser.add_argument('-u', '--update', action="store_true", default=False, help='Update existing installation instead of installing') parser.add_argument('-r', '--reinstall', action="store_true", default=False, help='Reinstall over existing installation (wipes database!)') parser.add_argument('--uninstall', action="store_true", default=False, help='Uninstall (wipes database!)') parser.add_argument('-o', '--hostname', type=str, help='Hostname of your machine. Defaults to localhost if not set.', required=False, default = "localhost") parser.add_argument('-p', '--http-proxy', type=str, help='http proxy in the format http://', default=default_http_proxy, required=False) parser.add_argument('-s', '--https-proxy', type=str, help='https proxy in the format http://', default=default_https_proxy, required=False) parser.add_argument('-S', '--socks-proxy', type=str, help='socks proxy in the format socks://myproxy:port>', default=default_socks_proxy, required=False) parser.add_argument('-N', '--no-proxy', type=str, help='Comma-separated list of hosts that should not be connected to via the proxy', default=default_no_proxy, required=False) parser.add_argument('-d', '--databasefile', type=str, help='Location of your database file to import. Must be a .sql, .sql.gz or .sql.zst file.', required=False) parser.add_argument('-e', '--email-host', type=str, help='Email host for sending messages (optionally with :port if not 25)', required=False) parser.add_argument('--email-user', type=str, help='User name to use when connecting to email host', required=False) parser.add_argument('--email-password', type=str, help='Password to use when connecting to email host', required=False) parser.add_argument('--email-ssl', action="store_true", default=False, help='Use SSL when connecting to email host') parser.add_argument('--email-tls', action="store_true", default=False, help='Use TLS when connecting to email host') parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80,8081:443') parser.add_argument('--project-name', type=str, help='docker-compose project name to use') parser.add_argument('--no-https', action="store_true", default=False, help='Disable HTTPS (HTTP only) for web server') parser.add_argument('--cert', type=str, help='Existing SSL certificate to use for HTTPS web serving', required=False) parser.add_argument('--cert-key', type=str, help='Existing SSL certificate key to use for HTTPS web serving', required=False) parser.add_argument('--letsencrypt', action="store_true", default=False, help='Use Let\'s Encrypt for HTTPS') parser.add_argument('--letsencrypt-production', action="store_true", default=False, help='Use Production server for Let\'s Encrypt. Default is %(default)s') parser.add_argument('--no-migrate', action="store_true", default=False, help='Skip running database migrations') parser.add_argument('--no-admin-user', action="store_true", default=False, help='Skip adding admin user') parser.add_argument('--no-connectivity', action="store_true", default=False, help='Skip checking external network connectivity') args = parser.parse_args() if args.update: if args.http_proxy != default_http_proxy or args.https_proxy != default_https_proxy or args.no_proxy != default_no_proxy or args.databasefile or args.no_https or args.cert or args.cert_key or args.letsencrypt: raise argparse.ArgumentTypeError("The -u/--update option will not update configuration or database content, and thus none of the other configuration options can be used in conjunction with it") if args.reinstall: raise argparse.ArgumentTypeError("The -u/--update and -r/--reinstall options are mutually exclusive") socks_proxy_port = socks_proxy_host = "" try: if args.socks_proxy: split = args.socks_proxy.split(":") socks_proxy_port = split[2] socks_proxy_host = split[1].replace("/", "") elif args.http_proxy: # Guess that this will work split = args.http_proxy.split(":") socks_proxy_port = '1080' socks_proxy_host = split[1].replace("/", "") except IndexError: raise argparse.ArgumentTypeError("socks_proxy must be in format socks://") if args.http_proxy and not args.https_proxy: args.https_proxy = args.http_proxy elif args.https_proxy and not args.http_proxy: args.http_proxy = args.https_proxy for entry in args.portmapping.split(','): if len(entry.split(":")) != 2: raise argparse.ArgumentTypeError("Port mapping must in the format HOST:CONTAINER. Ex: 8080:80. Multiple mappings should be separated by commas.") if args.no_https: if args.cert or args.cert_key or args.letsencrypt: raise argparse.ArgumentTypeError("--no-https and --cert/--cert-key/--letsencrypt options are mutually exclusive") if args.letsencrypt: if args.cert or args.cert_key: raise argparse.ArgumentTypeError("--letsencrypt and --cert/--cert-key options are mutually exclusive") if args.cert and not os.path.exists(args.cert): raise argparse.ArgumentTypeError("Specified certificate file %s does not exist" % args.cert) if args.cert_key and not os.path.exists(args.cert_key): raise argparse.ArgumentTypeError("Specified certificate key file %s does not exist" % args.cert_key) if args.cert_key and not args.cert: raise argparse.ArgumentTypeError("Certificate key file specified but not certificate") if args.cert and not args.cert_key: args.cert_key = os.path.splitext(args.cert)[0] + '.key' if not os.path.exists(args.cert_key): raise argparse.ArgumentTypeError("Could not find certificate key, please use --cert-key to specify it") email_host = None email_port = None if args.email_host: email_host_split = args.email_host.split(':') email_host = email_host_split[0] if len(email_host_split) > 1: email_port = email_host_split[1] if args.email_ssl and args.email_tls: raise argparse.ArgumentTypeError("--email-ssl and --email-tls options are mutually exclusive") if (args.email_ssl or args.email_tls or args.email_user or args.email_password) and not email_host: raise argparse.ArgumentTypeError("If any of the email host options are specified then you must also specify an email host with -e/--email-host") return args, socks_proxy_port, socks_proxy_host, email_host, email_port # Edit http_proxy and https_proxy in Dockerfile def edit_dockerfile(http_proxy, https_proxy, no_proxy): filedata= readfile("Dockerfile") newlines = [] lines = filedata.splitlines() for line in lines: if "ENV http_proxy" in line: if http_proxy: newlines.append("ENV http_proxy " + http_proxy + "\n") else: newlines.append('#' + line.lstrip('#') + '\n') elif "ENV https_proxy" in line: if https_proxy: newlines.append("ENV https_proxy " + https_proxy + "\n") else: newlines.append('#' + line.lstrip('#') + '\n') elif "ENV no_proxy" in line: if no_proxy: newlines.append("ENV no_proxy " + no_proxy + "\n") else: newlines.append('#' + line.lstrip('#') + '\n') else: newlines.append(line + "\n") writefile("Dockerfile", ''.join(newlines)) def convert_no_proxy(no_proxy): ''' Convert no_proxy to something that will work in a shell case statement (for the git proxy script) ''' no_proxy_sh = [] for item in no_proxy.split(','): ip_res = re.match('^([0-9]+).([0-9]+).([0-9]+).([0-9]+)/([0-9]+)$', item) if ip_res: mask = int(ip_res.groups()[4]) if mask == 8: no_proxy_sh.append('%s.*' % ip_res.groups()[0]) elif mask == 16: no_proxy_sh.append('%s.%s.*' % ip_res.groups()[:2]) elif mask == 24: no_proxy_sh.append('%s.%s.%s.*' % ip_res.groups()[:3]) elif mask == 32: no_proxy_sh.append('%s.%s.%s.%s' % ip_res.groups()[:4]) # If it's not one of these, we can't support it - just skip it else: if item.startswith('.'): no_proxy_sh.append('*' + item) else: no_proxy_sh.append(item) return '|'.join(no_proxy_sh) # If using a proxy, add proxy values to git-proxy and uncomment proxy script in .gitconfig def edit_gitproxy(socks_proxy_host, socks_proxy_port, no_proxy): no_proxy_sh = convert_no_proxy(no_proxy) filedata = readfile("docker/git-proxy") newlines = [] lines = filedata.splitlines() eatnextline = False for line in lines: if eatnextline: eatnextline = False continue if line.startswith('PROXY='): newlines.append('PROXY=' + socks_proxy_host + '\n') elif line.startswith('PORT='): newlines.append('PORT=' + socks_proxy_port + '\n') elif '## NO_PROXY' in line: newlines.append(line + '\n') newlines.append(' %s)\n' % no_proxy_sh) eatnextline = True else: newlines.append(line + "\n") writefile("docker/git-proxy", ''.join(newlines)) filedata = readfile("docker/.gitconfig") newlines = [] for line in filedata.splitlines(): if 'gitproxy' in line: if socks_proxy_host: newlines.append(yaml_uncomment(line) + "\n") else: newlines.append(yaml_comment(line) + "\n") else: newlines.append(line + "\n") writefile("docker/.gitconfig", ''.join(newlines)) def yaml_uncomment(line): out = '' for i, ch in enumerate(line): if ch == ' ': out += ch elif ch != '#': out += line[i:] break return out def yaml_comment(line): out = '' commented = False for i, ch in enumerate(line): if ch == '#': commented = True out += line[i:] break elif ch != ' ': if not commented: out += '#' out += line[i:] break else: out += ch return out # Add hostname, secret key, db info, and email host in docker-compose.yml def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey, rmqpassword, portmapping, letsencrypt, email_host, email_port, email_user, email_password, email_ssl, email_tls): def adjust_cert_mount_line(ln): linesplit = ln.split(':') if letsencrypt: linesplit[1] = '/etc/letsencrypt' else: linesplit[1] = '/opt/cert' # This allows us to handle if there is a ":ro" or similar on the end return ':'.join(linesplit) filedata= readfile("docker-compose.yml") in_layersweb = False in_layersweb_ports = False in_layersweb_ports_format = None in_layerscertbot_format = None newlines = [] lines = filedata.splitlines() for line in lines: if in_layersweb_ports: format = line[0:line.find("-")].replace("#", "") if in_layersweb_ports_format: if format != in_layersweb_ports_format: in_layersweb_ports = False in_layersweb = False else: continue else: in_layersweb_ports_format = format for portmap in portmapping.split(','): newlines.append(format + '- "' + portmap + '"' + "\n") continue if in_layerscertbot_format: ucline = yaml_uncomment(line) format = re.match(r'^( *)', ucline).group(0) if len(format) <= len(in_layerscertbot_format): in_layerscertbot_format = False elif letsencrypt: if "./docker/certs:/" in ucline: ucline = adjust_cert_mount_line(ucline) newlines.append(ucline + '\n') continue else: newlines.append(yaml_comment(line) + '\n') continue if "layerscertbot:" in line: ucline = yaml_uncomment(line) in_layerscertbot_format = re.match(r'^( *)', ucline).group(0) if letsencrypt: newlines.append(ucline + '\n') else: newlines.append(yaml_comment(line) + '\n') elif "layersweb:" in line: in_layersweb = True newlines.append(line + "\n") elif "hostname:" in line: format = line[0:line.find("hostname")].replace("#", "") newlines.append(format +"hostname: " + hostname + "\n") elif '- "SECRET_KEY' in line: format = line[0:line.find('- "SECRET_KEY')].replace("#", "") newlines.append(format + '- "SECRET_KEY=' + secretkey + '"\n') elif '- "DATABASE_USER' in line: format = line[0:line.find('- "DATABASE_USER')].replace("#", "") newlines.append(format + '- "DATABASE_USER=layers"\n') elif '- "DATABASE_PASSWORD' in line: format = line[0:line.find('- "DATABASE_PASSWORD')].replace("#", "") newlines.append(format + '- "DATABASE_PASSWORD=' + dbpassword + '"\n') elif '- "MYSQL_ROOT_PASSWORD' in line: format = line[0:line.find('- "MYSQL_ROOT_PASSWORD')].replace("#", "") newlines.append(format + '- "MYSQL_ROOT_PASSWORD=' + dbapassword + '"\n') elif '- "RABBITMQ_DEFAULT_USER' in line: format = line[0:line.find('- "RABBITMQ_DEFAULT_USER')].replace("#", "") newlines.append(format + '- "RABBITMQ_DEFAULT_USER=layermq"\n') elif '- "RABBITMQ_DEFAULT_PASS' in line: format = line[0:line.find('- "RABBITMQ_DEFAULT_PASS')].replace("#", "") newlines.append(format + '- "RABBITMQ_DEFAULT_PASS=' + rmqpassword + '"\n') elif '- "EMAIL_HOST' in line: format = line[0:line.find('- "EMAIL_HOST')].replace("#", "") if email_host: newlines.append(format + '- "EMAIL_HOST=' + email_host + '"\n') else: newlines.append(format + '#- "EMAIL_HOST="\n') elif '- "EMAIL_PORT' in line: format = line[0:line.find('- "EMAIL_PORT')].replace("#", "") if email_port: newlines.append(format + '- "EMAIL_PORT=' + email_port + '"\n') else: newlines.append(format + '#- "EMAIL_PORT="\n') elif '- "EMAIL_USER' in line: format = line[0:line.find('- "EMAIL_USER')].replace("#", "") if email_user: newlines.append(format + '- "EMAIL_USER=' + email_user + '"\n') else: newlines.append(format + '#- "EMAIL_USER="\n') elif '- "EMAIL_PASSWORD' in line: format = line[0:line.find('- "EMAIL_PASSWORD')].replace("#", "") if email_password: newlines.append(format + '- "EMAIL_PASSWORD=' + email_password + '"\n') else: newlines.append(format + '#- "EMAIL_PASSWORD="\n') elif '- "EMAIL_USE_SSL' in line: format = line[0:line.find('- "EMAIL_USE_SSL')].replace("#", "") if email_ssl: newlines.append(format + '- "EMAIL_USE_SSL=' + str(email_ssl) + '"\n') else: newlines.append(format + '#- "EMAIL_USE_SSL="\n') elif '- "EMAIL_USE_TLS' in line: format = line[0:line.find('- "EMAIL_USE_TLS')].replace("#", "") if email_tls: newlines.append(format + '- "EMAIL_USE_TLS=' + str(email_tls) + '"\n') else: newlines.append(format + '#- "EMAIL_USE_TLS="\n') elif "ports:" in line: if in_layersweb: in_layersweb_ports = True newlines.append(line + "\n") elif "./docker/certs:/" in line: newlines.append(adjust_cert_mount_line(line) + '\n') else: newlines.append(line + "\n") writefile("docker-compose.yml", ''.join(newlines)) def read_nginx_ssl_conf(certdir): hostname = None https_port = None certdir = None certfile = None keyfile = None with open('docker/nginx-ssl-edited.conf', 'r') as f: for line in f: if 'ssl_certificate ' in line: certdir, certfile = os.path.split(line.split('ssl_certificate', 1)[1].strip().rstrip(';')) elif 'ssl_certificate_key ' in line: keyfile = os.path.basename(line.split('ssl_certificate_key', 1)[1].strip().rstrip(';')) elif 'server_name ' in line: sname = line.split('server_name', 1)[1].strip().rstrip(';') if sname != '_': hostname = sname elif 'return 301 https://' in line: res = re.search(':([0-9]+)', line) if res: https_port = res.groups()[0] ret = (hostname, https_port, certdir, certfile, keyfile) if None in ret: sys.stderr.write('Failed to read SSL configuration from nginx-ssl-edited.conf') sys.exit(1) return ret def edit_nginx_ssl_conf(hostname, https_port, certdir, certfile, keyfile): filedata = readfile('docker/nginx-ssl.conf') newlines = [] lines = filedata.splitlines() for line in lines: if 'ssl_certificate ' in line: format = line[0:line.find('ssl_certificate')] newlines.append(format + 'ssl_certificate ' + os.path.join(certdir, certfile) + ';\n') elif 'ssl_certificate_key ' in line: format = line[0:line.find('ssl_certificate_key')] newlines.append(format + 'ssl_certificate_key ' + os.path.join(certdir, keyfile) + ';\n') # Add a line for the dhparam file newlines.append(format + 'ssl_dhparam ' + os.path.join(certdir, 'dhparam.pem') + ';\n') elif 'https://layers.openembedded.org' in line: line = line.replace('https://layers.openembedded.org', 'https://%s:%s' % (hostname, https_port)) newlines.append(line + "\n") else: line = line.replace('layers.openembedded.org', hostname) newlines.append(line + "\n") # Write to a different file so we can still replace the hostname next time writefile("docker/nginx-ssl-edited.conf", ''.join(newlines)) def edit_settings_py(emailaddr): filedata = readfile('docker/settings.py') newlines = [] lines = filedata.splitlines() in_admins = False for line in lines: if in_admins: if line.strip() == ')': in_admins = False continue elif line.lstrip().startswith('ADMINS = ('): if line.count('(') > line.count(')'): in_admins = True newlines.append("ADMINS = (\n") if emailaddr: newlines.append(" ('Admin', '%s'),\n" % emailaddr) newlines.append(")\n") continue newlines.append(line + "\n") writefile("docker/settings.py", ''.join(newlines)) def read_dockerfile_web(): no_https = True with open('Dockerfile.web', 'r') as f: for line in f: if line.startswith('COPY ') and line.rstrip().endswith('/etc/nginx/nginx.conf'): if 'nginx-ssl' in line: no_https = False break return no_https def edit_dockerfile_web(hostname, no_https): filedata = readfile('Dockerfile.web') newlines = [] lines = filedata.splitlines() for line in lines: if line.startswith('COPY ') and line.endswith('/etc/nginx/nginx.conf'): if no_https: srcfile = 'docker/nginx.conf' else: srcfile = 'docker/nginx-ssl-edited.conf' line = 'COPY %s /etc/nginx/nginx.conf' % srcfile newlines.append(line + "\n") writefile("Dockerfile.web", ''.join(newlines)) def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_production, cert, cert_key, emailaddr): local_cert_dir = os.path.abspath('docker/certs') container_cert_dir = '/opt/cert' if letsencrypt: # Create dummy cert container_cert_dir = '/etc/letsencrypt' letsencrypt_cert_subdir = 'live/' + hostname local_letsencrypt_cert_dir = os.path.join(local_cert_dir, letsencrypt_cert_subdir) if not os.path.isdir(local_letsencrypt_cert_dir): os.makedirs(local_letsencrypt_cert_dir) keyfile = os.path.join(letsencrypt_cert_subdir, 'privkey.pem') certfile = os.path.join(letsencrypt_cert_subdir, 'fullchain.pem') return_code = subprocess.call(['openssl', 'req', '-x509', '-nodes', '-newkey', 'rsa:2048', '-days', '1', '-keyout', os.path.join(local_cert_dir, keyfile), '-out', os.path.join(local_cert_dir, certfile), '-subj', '/CN=localhost'], shell=False) if return_code != 0: print("Dummy certificate generation failed") sys.exit(1) elif cert: if os.path.abspath(os.path.dirname(cert)) != local_cert_dir: shutil.copy(cert, local_cert_dir) certfile = os.path.basename(cert) if os.path.abspath(os.path.dirname(cert_key)) != local_cert_dir: shutil.copy(cert_key, local_cert_dir) keyfile = os.path.basename(cert_key) else: print('') print('Generating self-signed SSL certificate. Please specify your hostname (%s) when prompted for the Common Name.' % hostname) certfile = 'setup-selfsigned.crt' keyfile = 'setup-selfsigned.key' return_code = subprocess.call(['openssl', 'req', '-x509', '-nodes', '-days', '365', '-newkey', 'rsa:2048', '-keyout', os.path.join(local_cert_dir, keyfile), '-out', os.path.join(local_cert_dir, certfile)], shell=False) if return_code != 0: print("Self-signed certificate generation failed") sys.exit(1) return_code = subprocess.call(['openssl', 'dhparam', '-out', os.path.join(local_cert_dir, 'dhparam.pem'), '2048'], shell=False) if return_code != 0: print("DH group generation failed") sys.exit(1) edit_nginx_ssl_conf(hostname, https_port, container_cert_dir, certfile, keyfile) if letsencrypt: return_code = subprocess.call(['docker-compose', 'up', '-d', '--build', 'layersweb'], shell=False) if return_code != 0: print("docker-compose up layersweb failed") sys.exit(1) tempdir = tempfile.mkdtemp() try: # Wait for web server to start while True: time.sleep(2) return_code = subprocess.call(['wget', '-q', '--no-check-certificate', "http://{}:{}/".format(hostname, http_port)], shell=False, cwd=tempdir) if return_code == 0 or return_code > 4: break else: print("Web server may not be ready; will try again.") # Delete temp cert now that the server is up shutil.rmtree(os.path.join(local_cert_dir, 'live')) # Create a test file and fetch it to ensure web server is working (for http) return_code = subprocess.call("docker-compose exec -T layersweb /bin/sh -c 'mkdir -p /var/www/certbot/.well-known/acme-challenge/ ; echo something > /var/www/certbot/.well-known/acme-challenge/test.txt'", shell=True) if return_code != 0: print("Creating test file failed") sys.exit(1) return_code = subprocess.call(['wget', '-nv', "http://{}:{}/.well-known/acme-challenge/test.txt".format(hostname, http_port)], shell=False, cwd=tempdir) if return_code != 0: print("Reading test file from web server failed") sys.exit(1) return_code = subprocess.call(['docker-compose', 'exec', '-T', 'layersweb', '/bin/sh', '-c', 'rm -rf /var/www/certbot/.well-known'], shell=False) if return_code != 0: print("Removing test file failed") sys.exit(1) finally: shutil.rmtree(tempdir) # Now run certbot to register SSL certificate staging_arg = '--force-renewal --test-cert' if not letsencrypt_production else '--keep-until-expiring' if emailaddr: email_arg = '--email %s' % quote(emailaddr) else: email_arg = '--register-unsafely-without-email' return_code = subprocess.call('docker-compose run --rm --entrypoint "\ certbot certonly --webroot -w /var/www/certbot \ %s \ %s \ -d %s \ --rsa-key-size 4096 \ --agree-tos \ " layerscertbot' % (staging_arg, email_arg, quote(hostname)), shell=True) if return_code != 0: print("Running certbot failed") sys.exit(1) # Stop web server (so it can effectively be restarted with the new certificate) return_code = subprocess.call(['docker-compose', 'stop', 'layersweb'], shell=False) if return_code != 0: print("docker-compose stop failed") sys.exit(1) def edit_options_file(project_name): with open('.dockersetup-options', 'w') as f: f.write('project_name=%s\n' % project_name) def check_connectivity(): return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', '/opt/connectivity_check.sh'], shell=False) if return_code != 0: print("Connectivity check failed - if you are behind a proxy, please check that you have correctly specified the proxy settings on the command line (see --help for details)") sys.exit(1) def generatepasswords(passwordlength): return ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@%^&*-_+') for i in range(passwordlength)]) def readfile(filename): with open(filename, 'r') as f: return f.read() def writefile(filename, data): with open(filename, 'w') as f: f.write(data) ## Get user arguments try: args, socks_proxy_port, socks_proxy_host, email_host, email_port = get_args() except argparse.ArgumentTypeError as e: print('error: %s' % e) sys.exit(1) if args.update: with open('docker-compose.yml', 'r') as f: for line in f: if 'MYSQL_ROOT_PASSWORD=' in line: dbapassword = line.split('=')[1].rstrip().rstrip('"') break # Use last project name try: with open('.dockersetup-options', 'r') as f: for line in f: if line.startswith('project_name='): args.project_name = line.split('=', 1)[1].rstrip() except FileNotFoundError: pass else: # Generate secret key and database password secretkey = generatepasswords(50) dbapassword = generatepasswords(10) dbpassword = generatepasswords(10) rmqpassword = generatepasswords(10) if args.project_name: os.environ['COMPOSE_PROJECT_NAME'] = args.project_name else: # Get the project name from the environment (so we can save it for a future upgrade) args.project_name = os.environ.get('COMPOSE_PROJECT_NAME', '') https_port = None http_port = None if not args.update: for portmap in args.portmapping.split(','): outport, inport = portmap.split(':', 1) if inport == '443': https_port = outport elif inport == '80': http_port = outport if (not https_port) and (not args.no_https): print("No HTTPS port mapping (to port 443 inside the container) was specified and --no-https was not specified") sys.exit(1) if not http_port: print("Port mapping must include a mapping to port 80 inside the container") sys.exit(1) ## Check if it's installed installed = False return_code = subprocess.call("docker ps -a | grep -q layersapp", shell=True) if return_code == 0: installed = True if args.uninstall: if not installed: print("Cannot uninstall - application does not appear to be installed") sys.exit(1) elif args.update: if not installed: print("Application container not found - update mode can only be used on an existing installation") sys.exit(1) if dbapassword == 'testingpw': print("Update mode can only be used when previous configuration is still present in docker-compose.yml and other files") sys.exit(1) elif installed and not args.reinstall: print('Application already installed. Please use -u/--update to update or -r/--reinstall to reinstall') sys.exit(1) if not args.uninstall: print(""" OE Layer Index Docker setup script ---------------------------------- This script will set up a cluster of Docker containers needed to run the OpenEmbedded Layer Index application. Configuration is controlled by command-line arguments. If you need to check which options you need to specify, press Ctrl+C now and then run the script again with the --help argument. Note that this script does have interactive prompts, so be prepared to provide information as needed. """) if not (args.update or args.uninstall) and not email_host: print(""" WARNING: no email host has been specified - functions that require email (such as and new account registraion, password reset and error reports will not work without it. If you wish to correct this, press Ctrl+C now and then re-run specifying the email host with the --email-host option. """) if args.reinstall: print(""" WARNING: continuing will wipe out any existing data in the database and set up the application from scratch! Press Ctrl+C now if this is not what you want. """) if args.uninstall: print(""" WARNING: continuing will wipe out any existing data in the database and uninstall the application. Press Ctrl+C now if this is not what you want. """) try: if args.uninstall: promptstr = 'Press Enter to begin uninstallation (or Ctrl+C to exit)...' elif args.update: promptstr = 'Press Enter to begin update (or Ctrl+C to exit)...' else: promptstr = 'Press Enter to begin setup (or Ctrl+C to exit)...' input(promptstr) except KeyboardInterrupt: print('') sys.exit(2) if not (args.update or args.uninstall): # Get email address print('') if args.letsencrypt: print('You will now be asked for an email address. This will be used for the superuser account, to send error reports to and for Let\'s Encrypt.') else: print('You will now be asked for an email address. This will be used for the superuser account and to send error reports to.') emailaddr = None while True: emailaddr = input('Enter your email address: ') if '@' in emailaddr: break else: print('Entered email address is not valid') if args.reinstall or args.uninstall: return_code = subprocess.call(['docker-compose', 'down', '-v'], shell=False) if args.uninstall: # We're done print('Uninstallation completed') sys.exit(0) if args.update: args.no_https = read_dockerfile_web() if not args.no_https: container_cert_dir = '/opt/cert' args.hostname, https_port, certdir, certfile, keyfile = read_nginx_ssl_conf(container_cert_dir) edit_nginx_ssl_conf(args.hostname, https_port, certdir, certfile, keyfile) else: # Always edit these in case we switch from proxy to no proxy edit_gitproxy(socks_proxy_host, socks_proxy_port, args.no_proxy) edit_dockerfile(args.http_proxy, args.https_proxy, args.no_proxy) edit_dockercompose(args.hostname, dbpassword, dbapassword, secretkey, rmqpassword, args.portmapping, args.letsencrypt, email_host, email_port, args.email_user, args.email_password, args.email_ssl, args.email_tls) edit_dockerfile_web(args.hostname, args.no_https) edit_settings_py(emailaddr) edit_options_file(args.project_name) if not args.no_https: setup_https(args.hostname, http_port, https_port, args.letsencrypt, args.letsencrypt_production, args.cert, args.cert_key, emailaddr) ## Start up containers return_code = subprocess.call(['docker-compose', 'up', '-d', '--build'], shell=False) if return_code != 0: print("docker-compose up failed") sys.exit(1) if not (args.update or args.no_connectivity): ## Run connectivity check check_connectivity() # Get real project name (if only there were a reasonable way to do this... ugh) real_project_name = '' output = subprocess.check_output(['docker-compose', 'ps', '-q'], shell=False) if output: output = output.decode('utf-8') for contid in output.splitlines(): output = subprocess.check_output(['docker', 'inspect', '-f', '{{ .Mounts }}', contid], shell=False) if output: output = output.decode('utf-8') for volume in re.findall('volume ([^ ]+)', output): if '_' in volume: real_project_name = volume.rsplit('_', 1)[0] break if real_project_name: break if not real_project_name: print('Failed to detect docker-compose project name') sys.exit(1) # Database might not be ready yet; have to wait then poll. time.sleep(8) while True: time.sleep(2) # Pass credentials through environment for slightly better security # (avoids password being visible through ps or /proc//cmdline) env = os.environ.copy() env['MYSQL_PWD'] = dbapassword # Dummy command, we just want to establish that the db can be connected to return_code = subprocess.call("echo | docker-compose exec -T -e MYSQL_PWD layersdb mysql -uroot layersdb", shell=True, env=env) if return_code == 0: break else: print("Database server may not be ready; will try again.") if not args.update: # Import the user's supplied data if args.databasefile: filename, file_extension = os.path.splitext(args.databasefile) if file_extension == ".zst": return_code = subprocess.call("zstd -t %s > /dev/null 2>&1" % quote(args.databasefile), shell=True) if return_code == 0: catcmd = 'zstdcat' elif file_extension == ".gz": return_code = subprocess.call("gunzip -t %s > /dev/null 2>&1" % quote(args.databasefile), shell=True) if return_code == 0: catcmd = 'zcat' else: catcmd = 'cat' env = os.environ.copy() env['MYSQL_PWD'] = dbapassword return_code = subprocess.call("%s %s | docker-compose exec -T -e MYSQL_PWD layersdb mysql -uroot layersdb" % (catcmd, quote(args.databasefile)), shell=True, env=env) if return_code != 0: print("Database import failed") sys.exit(1) if not args.no_migrate: # Apply any pending layerindex migrations / initialize the database. env = os.environ.copy() env['DATABASE_USER'] = 'root' env['DATABASE_PASSWORD'] = dbapassword return_code = subprocess.call(['docker-compose', 'run', '--rm', '-e', 'DATABASE_USER', '-e', 'DATABASE_PASSWORD', 'layersapp', '/opt/migrate.sh'], shell=False, env=env) if return_code != 0: print("Applying migrations failed") sys.exit(1) if not args.update: # Create normal database user for app to use with tempfile.NamedTemporaryFile('w', dir=os.getcwd(), delete=False) as tf: sqlscriptfile = tf.name tf.write("DROP USER IF EXISTS layers;") tf.write("CREATE USER layers IDENTIFIED BY '%s';\n" % dbpassword) tf.write("GRANT SELECT, UPDATE, INSERT, DELETE ON layersdb.* TO layers;\n") tf.write("FLUSH PRIVILEGES;\n") try: # Pass credentials through environment for slightly better security # (avoids password being visible through ps or /proc//cmdline) env = os.environ.copy() env['MYSQL_PWD'] = dbapassword return_code = subprocess.call("docker-compose exec -T -e MYSQL_PWD layersdb mysql -uroot layersdb < " + quote(sqlscriptfile), shell=True, env=env) if return_code != 0: print("Creating database user failed") sys.exit(1) finally: os.remove(sqlscriptfile) ## Set the volume permissions using debian:stretch since we recently fetched it volumes = ['layersmeta', 'layersstatic', 'logvolume'] with open('docker-compose.yml', 'r') as f: for line in f: if line.lstrip().startswith('- srcvolume:'): volumes.append('srcvolume') break for volume in volumes: volname = '%s_%s' % (real_project_name, volume) return_code = subprocess.call(['docker', 'run', '--rm', '-v', '%s:/opt/mount' % volname, 'debian:stretch', 'chown', '500', '/opt/mount'], shell=False) if return_code != 0: print("Setting volume permissions for volume %s failed" % volume) sys.exit(1) ## Generate static assets. Run this command again to regenerate at any time (when static assets in the code are updated) return_code = subprocess.call("docker-compose run --rm -e STATIC_ROOT=/usr/share/nginx/html -v %s_layersstatic:/usr/share/nginx/html layersapp /opt/layerindex/manage.py collectstatic --noinput" % quote(real_project_name), shell = True) if return_code != 0: print("Collecting static files failed") sys.exit(1) if https_port and not args.no_https: protocol = 'https' port = https_port defport = '443' else: protocol = 'http' port = http_port defport = '80' if port == defport: host = args.hostname else: host = '%s:%s' % (args.hostname, port) if not args.update: if not args.databasefile: ## Set site name return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', '/opt/layerindex/layerindex/tools/site_name.py', host, 'OpenEmbedded Layer Index'], shell=False) if not args.no_admin_user: ## For a fresh database, create an admin account print("Creating database superuser. Input user name and password when prompted.") return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', '/opt/layerindex/manage.py', 'createsuperuser', '--email', emailaddr], shell=False) if return_code != 0: print("Creating superuser failed") sys.exit(1) if args.update: print("Update complete") else: if args.project_name: print("") print("NOTE: you may need to use -p %s (or set COMPOSE_PROJECT_NAME=\"%s\" ) if running docker-compose directly in future" % (args.project_name, args.project_name)) print("") print("The application should now be accessible at %s://%s" % (protocol, host)) print("")