#! /usr/bin/python3 -s
#
# Copyright (c) 2010 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

"""
Script to set up a Candlepin server.

This script should be idempotent, as puppet will re-run this to keep
the server functional.
"""

from __future__ import print_function
from optparse import OptionParser

import sys
import os.path, os
import socket
import re
import time

try:
    from commands import getstatusoutput
except ImportError:
    from subprocess import getstatusoutput

try:
    from httplib import HTTPConnection
except ImportError:
    from http.client import HTTPConnection

CANDLEPIN_CONF = '/etc/candlepin/candlepin.conf'

if os.path.exists('/usr/sbin/tomcat') and not os.path.exists('/usr/sbin/tomcat6'):
    TOMCAT = 'tomcat'
else:
    TOMCAT = 'tomcat6'


def run_command(command):
    (status, output) = getstatusoutput(command)
    if status > 0:
        sys.stderr.write("\n########## ERROR ############\n")
        sys.stderr.write("Error running command: %s\n" % command)
        sys.stderr.write("Status code: %s\n" % status)
        sys.stderr.write("Command output: %s\n" % output)
        raise Exception("Error running command")
    return output


# run with 'sudo' if not running as root
def run_command_with_sudo(command):
    if os.geteuid()==0:
        run_command(command)
    else:
        run_command('sudo %s' % command)


class TomcatSetup(object):
    def __init__(self, conf_dir, keystorepwd):
        self.conf_dir = conf_dir
        self.comment_pattern = '<!--\s*?\n*?<Connector port="8443".*?-->'
        self.existing_pattern = '<Connector port="8443".*?/>'
        self.https_conn = """
    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="want"
               sslEnabledProtocols="TLSv1,TLSv1.1,TLSv1.2"
               keystoreFile="conf/keystore"
               truststoreFile="conf/keystore"
               keystorePass="%s"
               keystoreType="PKCS12"
               ciphers="SSL_RSA_WITH_3DES_EDE_CBC_SHA,
                    TLS_RSA_WITH_AES_256_CBC_SHA,
                    TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA,
                    TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA,
                    TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
                    TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,
                    TLS_ECDH_RSA_WITH_AES_256_CBC_SHA,
                    TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA,
                    TLS_ECDH_anon_WITH_AES_128_CBC_SHA,
                    TLS_ECDH_anon_WITH_AES_256_CBC_SHA"
               truststorePass="%s" />""" % (keystorepwd, keystorepwd)
        self.main_conf = '/etc/tomcat/tomcat.conf'
        self.main_comment_pattern = '^#\s*JAVA_HOME.*'
        self.main_existing_pattern = '^JAVA_HOME.*'
        self.new_java_home = 'JAVA_HOME="/usr/lib/jvm/jre-11-openjdk"'

    def _backup_config(self, conf_dir):
        run_command('cp %s/server.xml %s/server.xml.original' % (conf_dir, conf_dir))

    def _replace_current(self, original):
        regex = re.compile(self.existing_pattern, re.DOTALL)
        return regex.sub(self.https_conn, original, 1)

    def _replace_commented(self, original):
        regex = re.compile(self.comment_pattern, re.DOTALL)
        return regex.sub(self.https_conn, original, 1)

    def _replace_current_main(self, original):
        regex = re.compile(self.main_existing_pattern, re.MULTILINE)
        return regex.sub(self.new_java_home, original, 1)

    def _replace_commented_main(self, original):
        regex = re.compile(self.main_comment_pattern, re.MULTILINE)
        return regex.sub(self.new_java_home, original, 1)

    def update_config(self):
        # Edit server.xml
        self._backup_config(self.conf_dir)
        original_file = open(os.path.join(self.conf_dir, 'server.xml'), 'r')
        original = original_file.read()

        if re.search(self.comment_pattern, original, re.DOTALL):
            updated = self._replace_commented(original)
        else:
            updated = self._replace_current(original)
        original_file.close()
        with open(os.path.join(self.conf_dir, 'server.xml'), 'w') as config:
            config.write(updated)

        # Edit tomcat.conf
        with open(self.main_conf, 'r') as original_main_file:
            original_main = original_main_file.read()

        if re.search(self.main_existing_pattern, original_main, re.MULTILINE):
            updated_main = self._replace_current_main(original_main)
        elif re.search(self.main_comment_pattern, original_main, re.MULTILINE):
            updated_main = self._replace_commented_main(original_main)
        else:
            updated_main = original_main + os.linesep + self.new_java_home

        with open(self.main_conf, 'w') as main_config:
            main_config.write(updated_main)

    def fix_perms(self):
        run_command("chmod g+x /var/log/" + TOMCAT)
        run_command("chmod g+x /etc/" + TOMCAT + "/")
        run_command("chown tomcat:tomcat -R /var/lib/" + TOMCAT)
        run_command("chown tomcat:tomcat -R /var/lib/" + TOMCAT)
        run_command("chown tomcat:tomcat -R /var/cache/" + TOMCAT)

    def stop(self):
        run_command("/sbin/service " + TOMCAT + " stop")

    def restart(self):
        run_command("/sbin/service " + TOMCAT + " restart")

    def wait_for_startup(self):
        print("Waiting for tomcat to restart...")
        conn = HTTPConnection('localhost:8080')
        for x in range(1,5):
            time.sleep(5)
            try:
                conn.request('GET', "/candlepin/")
                if conn.getresponse().status == 200:
                    break
            except:
                print("Waiting for tomcat to restart...")


class CertSetup(object):
    def __init__(self):
        self.cert_home = '/etc/candlepin/certs'
        self.ca_key_passwd = self.cert_home + '/candlepin-ca-password.txt'
        self.ca_key = self.cert_home + '/candlepin-ca.key'
        self.ca_pub_key = self.cert_home + '/candlepin-ca-pub.key'
        self.ca_cert = self.cert_home + '/candlepin-ca.crt'
        self.keystore = self.cert_home + '/keystore'

    def generate(self):
        if not os.path.exists(self.cert_home):
            run_command_with_sudo('mkdir -p %s' % self.cert_home)

        if os.path.exists(self.ca_key) and os.path.exists(self.ca_cert):
            print("Certificates already exist, skipping...")
            return

        print("Creating CA private key password")
        run_command_with_sudo('su -c "echo $RANDOM > %s"' % self.ca_key_passwd)

        print("Creating CA private key")
        run_command_with_sudo('openssl genrsa -out %s -passout "file:%s" 2048'
                              % (self.ca_key, self.ca_key_passwd))

        print("Creating CA public key")
        run_command_with_sudo('openssl rsa -pubout -in %s -out %s' % (self.ca_key, self.ca_pub_key))

        print("Creating CA certificate")
        run_command_with_sudo('openssl req -new -x509 -days 365 -key %s -out %s '
                              '-subj "/CN=%s/C=US/L=Raleigh/"'
                              % (self.ca_key, self.ca_cert, socket.gethostname()))
        run_command_with_sudo('openssl pkcs12 -export -in %s -inkey %s -out %s -name tomcat -CAfile %s '
                              '-caname root -chain -password pass:password'
                              % (self.ca_cert, self.ca_key, self.keystore, self.ca_cert))

        # Tomcat 8.5+ requires the cert be separate from the key when used
        # as both a keystore and a trust store
        run_command_with_sudo('keytool -keystore %s -noprompt -importcert -storepass password '
                              '-alias candlepin_ca -file %s' % (self.keystore, self.ca_cert))
        run_command_with_sudo('chmod a+r %s/*' % self.cert_home)


class PostgresqlConf(object):
    def __init__(self, options):
        self.options = options
        self.dialect = "org.hibernate.dialect.PostgreSQL92Dialect"
        self.driver = "org.postgresql.Driver"

        # Build up the correct jdbc URL:
        # TODO: duplicated within cpdb:
        self.jdbc_url = "jdbc:postgresql:"
        if self.options.dbhost is not None:
            self.jdbc_url = "%s//%s" % (self.jdbc_url, self.options.dbhost)
            # Requires host:
            if self.options.dbport is not None:
                self.jdbc_url = "%s:%s" % (self.jdbc_url, self.options.dbport)
            # Append / for the database name:
            self.jdbc_url = "%s/" % (self.jdbc_url)
        self.jdbc_url = "%s%s" % (self.jdbc_url, self.options.db)


def write_candlepin_conf(options):
    """
    Write configuration to candlepin.conf.
    """

    # If the file exists and it's size is not 0 (it will be empty after
    # fresh rpm install), write out a default with database configuration:
    if os.path.exists(CANDLEPIN_CONF) and os.stat(CANDLEPIN_CONF).st_size > 0:
        print("candlepin.conf already exists, skipping...")
        return

    print("Writing configuration file")

    dbconf = PostgresqlConf(options)

    f = open(CANDLEPIN_CONF, 'w')
    f.write('jpa.config.hibernate.dialect=%s\n' % dbconf.dialect)
    f.write('jpa.config.hibernate.connection.driver_class=%s\n' % dbconf.driver)
    f.write('jpa.config.hibernate.connection.url=%s\n' % dbconf.jdbc_url)
    f.write('jpa.config.hibernate.connection.username=%s\n' % options.dbuser)
    f.write('jpa.config.hibernate.connection.password=%s\n' % options.password)
    if options.webapp_prefix:
        f.write('\ncandlepin.export.webapp.prefix=%s\n' % options.webapp_prefix)
    f.close()


def main(argv):

    parser = OptionParser()
    parser.add_option("-s", "--skipdbcfg",
                  action="store_true", dest="skipdbcfg", default=False,
                  help="don't configure the /etc/candlepin/candlepin.conf file")
    parser.add_option("-u", "--user",
                  dest="dbuser", default=os.getenv('CANDLEPIN_DB_USER', 'candlepin'),
                  help="Database user. When missing environment variable 'CANDLEPIN_DB_USER' is read. Defaults to 'candlepin'.")
    parser.add_option("-d", "--database",
                  dest="db", default=os.getenv('CANDLEPIN_DB_NAME', 'candlepin'),
                  help="Database name. When missing environment variable 'CANDLEPIN_DB_NAME' is read. Defaults to 'candlepin'.")
    parser.add_option("--dbhost",
                  dest="dbhost",
                  help="the database host to use (optional)")
    parser.add_option("--dbport",
                  dest="dbport",
                  help="the database port to use (optional)")
    parser.add_option("--schema-only",
                  action="store_true", dest="schema_only", default=False,
                  help="database already exists, only load the schema.")
    parser.add_option("-w", "--webapp-prefix",
                  dest="webapp_prefix",
                  help="the web application prefix to use for export origin [host:port/prefix]")
    parser.add_option("-k", "--keystorepwd",
                  dest="keystorepwd", default="password",
                  help="the keystore password to use for the tomcat configuration")
    parser.add_option("-p", "--password",
                  dest="password", default=os.getenv('CANDLEPIN_DB_PASSWORD', 'candlepin'),
                  help="Database password. When missing environment variable 'CANDLEPIN_DB_PASSWORD' is read. Defaults to 'candlepin'.")
    parser.add_option("--skip-service", dest="skip_service", action="store_true",
        default=False, help="Skip attempting to stop/restart tomcat service.")

    (options, args) = parser.parse_args()

    # Stop tomcat before we wipe the DB otherwise you get errors from pg
    tsetup = TomcatSetup('/etc/' + TOMCAT, options.keystorepwd)
    tsetup.fix_perms()
    if not options.skip_service:
        tsetup.stop()

    # Call the cpdb script to create the candlepin database. Database creation will be
    # skipped if it already exists. The --schema-only option can be used if the database
    # is expected to have been created by an external script, and only the schema is to
    # be applied.
    script_dir = os.path.dirname(__file__)
    cpdb_script = os.path.join(script_dir, "cpdb")
    command = "%s --create -u %s --database %s --password %s" % (cpdb_script,
        options.dbuser, options.db, options.password)

    if options.schema_only:
        command = " ".join([command, "--schema-only"])

    if options.dbhost:
        command = " ".join([command, "--dbhost %s" % options.dbhost])
    if options.dbport:
        command = " ".join([command, "--dbport %s" % options.dbport])

    print(command)

    run_command(command)

    if not options.skipdbcfg:
        write_candlepin_conf(options)
    else:
        print("** Skipping configuration file setup")

    certsetup = CertSetup()
    certsetup.generate()

    tsetup = TomcatSetup('/etc/' + TOMCAT, options.keystorepwd)
    tsetup.update_config()

    if not options.skip_service:
        tsetup.restart()
        tsetup.wait_for_startup()
        run_command("wget -qO- http://localhost:8080/candlepin/admin/init")

    print("Candlepin has been configured.")

if __name__ == "__main__":
    main(sys.argv[1:])
