#!/usr/bin/python3
#
# 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, **kwargs):
    if os.geteuid()==0:
        output = run_command(command.format(**kwargs))
    else:
        output = run_command('sudo %s' % command.format(**kwargs))

    return output


class TomcatSetup(object):
    def __init__(self, conf_dir, keystorepwd):
        self.conf_dir = conf_dir
        self.comment_pattern = '<!--\s*?<Connector port="8443".*?-->'
        self.existing_pattern = '<Connector port="8443".*?</Connector>|<Connector port="8443".*?/>'
        self.https_conn = """
        <Connector port="8443" protocol="HTTP/1.1"
            scheme="https"
            secure="true"
            SSLEnabled="true"
            maxThreads="150">

            <SSLHostConfig certificateVerification="optional"
                protocols="+TLSv1,+TLSv1.1,+TLSv1.2"
                sslProtocol="TLS">

                <Certificate
                    certificateFile="/etc/candlepin/certs/candlepin-ca.crt"
                    certificateKeyFile="/etc/candlepin/certs/candlepin-ca.key"
                    type="RSA"
                />
            </SSLHostConfig>
        </Connector>"""
        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-17-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 _write_disable_fips_conf(self):
        filename = "candlepin_disable_fips.conf"
        confd_dir = os.path.join(self.conf_dir, "conf.d")
        target_file = os.path.join(confd_dir, filename)

        with open(target_file, 'w') as file:
            file.write("JAVA_OPTS=\"$JAVA_OPTS -Dcom.redhat.fips=false\"\n")

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

        # TODO: This isn't consistent. It will replace a commented-out version of the config
        # before replacing an uncommented version of it, but due to regex limitations, attempting
        # to flip this will just replace commented out blocks as if they weren't commented out.
        # Replace this with a proper html/xml node parser.
        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)

        # Write extra config file to conf.d to disable FIPS in this TC instance:
        self._write_disable_fips_conf()

    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.cert_name = 'candlepin-ca'

        self.noise_file = '{cert_home}/noise.bin'.format(cert_home=self.cert_home)
        self.password_file = '{cert_home}/cppw.txt'.format(cert_home=self.cert_home)

        self.req_config_file = '{cert_home}/req.cnf'.format(cert_home=self.cert_home)
        self.ca_key = '{cert_home}/{cert_name}.key'.format(cert_home=self.cert_home, cert_name=self.cert_name)
        self.ca_pub_key = '{cert_home}/{cert_name}-pub.key'.format(cert_home=self.cert_home, cert_name=self.cert_name)
        self.ca_cert = '{cert_home}/{cert_name}.crt'.format(cert_home=self.cert_home, cert_name=self.cert_name)

        self.ca_cert_days = 365
        self.cert_bit_length = 4096

    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

        args = {
            'hostname': socket.gethostname(),
            'interface_ip': run_command_with_sudo('hostname -I | cut -d \' \' -f 1'),
            'cert_home': self.cert_home,
            'cert_name': self.cert_name,
            'noise_file': self.noise_file,
            'password_file': self.password_file,
            'req_config_file': self.req_config_file,
            'ca_key': self.ca_key,
            'ca_pub_key': self.ca_pub_key,
            'ca_cert': self.ca_cert,
            'ca_cert_days': self.ca_cert_days,
            'cert_bit_length': self.cert_bit_length,
        }

        print("Generating CA certs and keys... ")

        # Commands copied directly from gen_certs.sh
        run_command_with_sudo('rm -rf {cert_home}', **args)
        run_command_with_sudo('mkdir -p {cert_home}', **args)
        run_command_with_sudo('openssl rand -out {noise_file} {cert_bit_length}', **args)
        run_command_with_sudo('openssl rand -base64 -out {password_file} 24', **args)
        run_command_with_sudo('openssl genpkey -out {ca_key} -pass "file:{password_file}" -algorithm rsa -pkeyopt rsa_keygen_bits:{cert_bit_length}', **args)
        run_command_with_sudo('openssl pkey -pubout -in {ca_key} -out {ca_pub_key}', **args)

        # Write cert generation config
        with open(self.req_config_file, 'w') as cnf:
            cnf.write('[req]\n')
            cnf.write('distinguished_name = req\n')
            cnf.write('x509_extensions = v3_ext\n')
            cnf.write('[v3_ext]\n')
            cnf.write('subjectKeyIdentifier = hash\n')
            cnf.write('authorityKeyIdentifier = keyid, issuer\n')
            cnf.write('basicConstraints = critical, CA:true\n')
            cnf.write('subjectAltName = @san\n')
            cnf.write('[san]\n')
            cnf.write('IP.1 = 127.0.0.1\n')
            cnf.write('IP.2 = {interface_ip}\n'.format(**args))
            cnf.write('DNS.1 = localhost\n')
            cnf.write('DNS.2 = {hostname}\n'.format(**args))

        run_command_with_sudo('openssl req -new -x509 -days {ca_cert_days} -key {ca_key} -out {ca_cert} ' +
            '-subj "/CN={hostname}/C=US/L=Raleigh/" -config {req_config_file}', **args)
        run_command_with_sudo('chmod a+r {cert_home}/*', **args)
        run_command_with_sudo('cp -f {ca_cert} "/etc/pki/ca-trust/source/anchors/{cert_name}.crt"', **args)
        run_command_with_sudo('update-ca-trust', **args)


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.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:])
