"""
provides utility functions for specific engine upgrade 3.0 -> 3.1
"""

import os
import shutil
import logging
import socket
import traceback

import common_utils as utils
import basedefs
import basedefs30
import output_messages30

def backupOldInstallation():
    '''
    Backup old installation to TAR file
    '''

    # Add PKI folder to tar list
    # Add configuration folder to tar list
    # Add JBoss folder to tar list, this includes dwh and reports
    backup_list = [
        basedefs30.DIR_RHEVM_PKI,
        basedefs30.DIR_BASE_CONF,
    ]

    # tar everything recursively, preserve full path and all the permissions
    cmd = [
        "/bin/tar",
        "cpPjf",
        basedefs30.OLD_VERSION_FILES,
        ] + backup_list
    utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: can't backup previous installation")

    # Backup krb5
    if os.path.exists(basedefs30.OLD_KRB5):
        shutil.copy(basedefs30.OLD_KRB5, "/var/")

def renameConfigDirectories():
    # Rename configuration and pki folders, since we will need them for upgrade
    folders = [
        basedefs30.DIR_RHEVM_PKI,
        basedefs30.DIR_BASE_CONF,
    ]

    for folder in folders:
        newFolder = folder + "-old"

        # This usually happens after a failed upgrade
        if os.path.exists(newFolder):
            newFolderWithDate = "%s.%s" % (newFolder, utils.getCurrentDateTime())
            logging.debug("%s already exists, renaming to %s" % (newFolder, newFolderWithDate))
            os.rename(newFolder, newFolderWithDate)

        logging.debug("Renaming %s to %s" % (folder, newFolder))
        os.rename(folder, newFolder)

def createDbUser():

    # backup existing .pgpass

    if not os.path.exists(basedefs.DB_PASS_FILE):
        logging.error("Error: pgpass file %s was not found.", basedefs.DB_PASS_FILE)
        raise Exception("Error: pgpass file %s was not found.", basedefs.DB_PASS_FILE)

    backupFile = "%s.%s" % (basedefs.DB_PASS_FILE, utils.getCurrentDateTime())
    logging.debug("found existing pgpass file, backing current to %s" % (backupFile))
    shutil.copy(basedefs.DB_PASS_FILE, backupFile)

    # copy rhevm line to engine line
    pgpass = None
    with open(basedefs.DB_PASS_FILE, "r") as p:
        pgpass = p.readlines()

    with open(basedefs.DB_PASS_FILE, "w") as p:
        for line in pgpass:
            if "rhevm" in line:
                p.write(line.replace("rhevm:rhevm", "*:%s" % basedefs.DB_USER))
            p.write(line)

def modifyDb(adminUser, password):

    # .pgpass definition
    env = { "PGPASSFILE" : basedefs.DB_PASS_FILE }

    # Rename DATABASE
    query = "ALTER DATABASE rhevm RENAME TO %s;" % basedefs.DB_NAME
    cmd = [
        "/usr/bin/psql",
        "-U", basedefs.DB_ADMIN,
        "-c", query,
    ]
    utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: failed to rename DB to %s" % basedefs.DB_NAME, envDict=env)

    # Rename DB user to new user
    query = "ALTER ROLE rhevm RENAME TO %s;" % basedefs.DB_USER
    cmd = [
        "/usr/bin/psql",
        "-U", basedefs.DB_ADMIN,
        "-c", query,
    ]
    utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: couldn't rename DB ROLE 'rhevm' to '%s'" % basedefs.DB_USER, envDict=env)

    # Encrypt DB user password
    query = "ALTER ROLE %s WITH ENCRYPTED PASSWORD '%s';" % (basedefs.DB_USER, password)
    cmd = [
        "/usr/bin/psql",
        "-U", basedefs.DB_ADMIN,
        "-c", query,
    ]
    utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: couldn't rename DB ROLE 'rhevm' to '%s'" % basedefs.DB_USER, envDict=env, maskList=[password])

    # Change all objects in rhevm DB to be owned by engine
    query_tables = "select tablename from pg_tables where schemaname = 'public';"
    query_schemas = "select sequence_name from information_schema.sequences where sequence_schema = 'public';"
    query_views = "select table_name from information_schema.views where table_schema = 'public'"

    for query in (query_tables, query_schemas, query_views):
        cmd = [
            basedefs.EXEC_PSQL,
            "-qAt",
            "-U", adminUser,
            "-d", basedefs.DB_NAME,
            "-c ", query,
        ]
        objects, rc = utils.execCmd(cmdList=cmd, failOnError=True, envDict=env)
        for dbobject in objects.split('\n'):
            if dbobject:
                upd_query = "alter table %s owner to %s" % (dbobject, basedefs.DB_USER)
                cmd = [
                    basedefs.EXEC_PSQL,
                    "-U", adminUser,
                    "-d", basedefs.DB_NAME,
                    "-c", upd_query,
                ]
                utils.execCmd(cmdList=cmd, failOnError=True, envDict=env)

def generateAnswerFile(password):
    # Generate a tmp answer file

    with open(basedefs30.ANSWER_FILE, "w") as f:
        f.write(
            basedefs30.ANSWER_FILE_TEMPLATE %
            (
                utils.generateMacRange(),
                socket.getfqdn(),
                password,
                password,
            )
        )

def getJboss5Packages():
    pkgsremove = []

    # Get all rpms in the system
    cmd = [basedefs.EXEC_RPM,"-qa"]
    allpkgs, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: couldn't retrieve a list of packages of current installation")
    allpkgs = allpkgs.split('\n')

    # Create EP5 packages list
    for pkg in allpkgs:
        if "ep5.el6" in pkg:
            pkgsremove.append(pkg)

    return pkgsremove

def removeJboss5():

    try:
        # Get the list of pkgs to remove
        pkgsremove = getJboss5Packages()

        # Remove everything, including deps
        cmd = [
            basedefs.EXEC_YUM,
            "remove",
            "-y",
        ] + pkgsremove
        utils.execCmd(cmdList=cmd, failOnError=True, msg=output_messages30.MSG_ERROR_REMOVE_PREVIOUS)
    except:
        raise Exception(output_messages30.MSG_ERROR_REMOVE_PREVIOUS)

def isUpdateFrom30():
    # Check upgrade path (3.1 -> 3.1; 3.0 -> 3.1)
    # This script comes in 3.1, so it is enough to
    # check the installed version.
    cmd = [basedefs.EXEC_RPM, "-q", "rhevm"]
    output, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg=output_messages30.MSG_ERROR_RPM_QUERY)
    return output.startswith("rhevm-3.0")

def jasperInstalled(adminUser):
    # Check that Jasper is not installed
    query = "select 1;"
    cmd = [
        basedefs.EXEC_PSQL,
        "-U", adminUser,
        "-c", query,
        "-d", basedefs30.REPORTS_DB_NAME,
    ]
    output, rc_db = utils.execCmd(cmdList = cmd, failOnError=False, msg=output_messages30.MSG_ERROR_QUERY_REPORTS_DB)

    if os.path.exists(basedefs30.JS_WAR_PATH) and rc_db == 0:
        return True

    return False

def upgrade(yum, rpmsList, installedRpmsList, dbBackupName):

    try:
        # Remove Jboss5 installation, includes rhevm!
        removeJboss5()

        # start install
        logging.debug("Yum install started")
        cmd = [basedefs.EXEC_YUM, "install", "-q", "-y"] + rpmsList.split()
        utils.execCmd(cmdList=cmd, failOnError=True, msg="Error: Yum update failed")

        # After successful installation, recreate the locking list
        yum._lock()

    except:
        logging.error(traceback.format_exc())

        # Remove rhevm-setup from the list of the installed rpms, because before
        # the upgrade is complete the setup is in higher version than the rest
        # of the rhevm. This list will be printed to the user so that she could
        # perform the installation of the exact previous version.
        for pkg in installedRpmsList:
            if "rhevm-setup" in pkg:
                installedRpmsList.remove(pkg)
                break
        raise Exception(output_messages30.MSG_ERROR_UPGRADE_FAILED)

def depsWhiteListed(rhyum):
    # Check that all the deps are whitelisted
    deplist = []

    # Remove "rhevm" from the deplist
    # TODO: if other 3rd party sofware needs to be whitelisted,
    # add the check here.
    for pkg in rhyum.depListForRemoval(getJboss5Packages()):
        if not "rhevm" in pkg:
            deplist.append(" * %s" % pkg)

    # If there's no deps other than whitelist, return True
    if len(deplist) == 0:
        return True

    # Otherwise, ask the user to proceed
    MESSAGE_WITH_DEPS = output_messages30.MSG_WARN_UPGRADE_WHITELIST % "\n".join(deplist)
    question = MESSAGE_WITH_DEPS + output_messages30.MSG_INFO_PROCEED
    proceed = utils.askYesNo(question)
    if not proceed:
        return False

    return True

def configureEngineForMaintenance():
    """
    Configures the JBoss 5 application server for maintenance.

    In order to do this we need to change the ports used by the application
    server. That way no one will be able to connect to the server (unless
    they guess the new port numbers) while we are performing maintenance
    task.
    """

    # First check that the file that we are trying to modify does exist:
    originalPath = basedefs30.JBOSS_5_SERVER_XML_PATH
    if not os.path.exists(originalPath):
        raise Exception("The file \"%s\" doesn't exist.", originalPath)

    # Make a backup copy that we will later restore when going out of
    # the maintenance mode, but only if it doesn't already exist, as
    # otherwise we loose the original content:
    backupPath = originalPath + ".backup"
    if not os.path.exists(backupPath):
        logging.debug("Making a backup copy \"%s\" to \"%s\".", originalPath, backupPath)
        utils.copyFile(originalPath, backupPath)

    # All edits will be performed in a temporary file, and only if
    # everything works correctly will the original be replaced:
    temporaryPath = originalPath + ".tmp"
    utils.copyFile(originalPath, temporaryPath)

    # Edit the server.xml file as required, then replace the original and
    # always remember to remove the temporary file when finished:
    try:
        logging.debug("Updating \"%s\".", temporaryPath)
        editServerXmlForMaintenance(temporaryPath)
        logging.debug("Replacing \"%s\" with \"%s\".", originalPath, temporaryPath)
        os.rename(temporaryPath, originalPath)
    finally:
        if os.path.exists(temporaryPath):
            os.remove(temporaryPath)

def restoreEngineFromMaintenance():
    """
    Restores the JBoss 5 configuration from maintentance.

    This means restoring the backup copies of the configuration files that have
    been modified.
    """

    # First check that the backup file that we need does exist:
    originalPath = basedefs30.JBOSS_5_SERVER_XML_PATH
    backupPath = originalPath + ".backup"
    if not os.path.exists(backupPath):
        raise Exception("Backup copy \"%s\" of \"%s\" doesn't exist.", backupPath, originalPath)

    # Replace the original with the backup, that's it:
    logging.debug("Replacing \"%s\" with \"%s\".", originalPath, backupPath)
    os.rename(backupPath, originalPath)

def editServerXmlForMaintenance(path):
    """
    Configures the JBoss 5 server.xml file for maintenance.

    The full path of the server.xml file is provided as a parameter, the
    function will not try to locate it.
    """

    def findFreePort(start):
        # Find a port that is free, starting from the one given (Linux kernel
        # allocates ephemeral ports starting with 32768, so that is better
        # avoided):
        port = start
        while port < 32768:
            open, _, _ = utils.isTcpPortOpen(port)
            if open:
                logging.debug("Port %d is in use, will try next one.", port)
                port += 1
            else:
                logging.debug("Port %d is not in use, will use it.", port)
                return port

        # Sorry, no port available:
        raise Exception("Can't find a free port.")

    # Compute the new port numbers to use (starting with 10000 looks
    # reasonable, as ports that high aren't probably in use):
    httpPort = findFreePort(10000)
    httpsPort = findFreePort(httpPort + 1)
    ajpPort = findFreePort(httpsPort + 1)
    logging.debug("Ports used will be %d for HTTP, %d for HTTPS and %d for AJP.", httpPort, httpsPort, ajpPort)

    # Load the XML document:
    logging.debug("Loading XML document from \"%s\".", path)
    handler = utils.XMLConfigFileHandler(path)
    handler.open()

    def changePort(protocol, xpath, port):
        # Find the XML element containing the connector:
        nodes = handler.xpathEval(xpath)
        if len(nodes) == 0:
            raise Exception("Can't find the %s connector in file \"%s\"." % (protocol, path))
        elif len(nodes) > 1:
            raise Exception("There are more than one %s connectors in file \"%s\"." % (protocol, path))
        node = nodes[0]

        # Replace the port number:
        logging.debug("Changing %s port to %d.", protocol, port)
        node.setProp("port", str(port))

    # Change the HTTP port:
    changePort(
        xpath="//Connector[@protocol='HTTP/1.1' and not (@SSLEnabled='true')]",
        protocol="HTTP",
        port=httpPort)

    # Change the HTTPS port:
    changePort(
        xpath="//Connector[@protocol='HTTP/1.1' and (@SSLEnabled='true')]",
        protocol="HTTPS",
        port=httpsPort)

    # Change the AJP port:
    changePort(
        xpath="//Connector[@protocol='AJP/1.3']",
        protocol="AJP",
        port=ajpPort)

    # Close and save the modified file:
    logging.debug("Saving XML document to \"%s\".", path)
    handler.close()

