#!/usr/bin/env python3
# -*- coding:utf-8 -*-
#############################################################################
# Copyright (c): 2012-2017, Huawei Tech. Co., Ltd.
# Description  : OMCommand.py is utility to execute the OM command
#############################################################################
try:
    import os
    import socket
    import sys
    import time
    import re
    import subprocess
    from multiprocessing.dummy import Pool as ThreadPool

    sys.path.append(sys.path[0] + "/../../")
    from gspylib.common.DbClusterInfo import dbClusterInfo
    from gspylib.common.Common import DefaultValue, ClusterCommand, TempfileManagement
    from gspylib.common.DbClusterStatus import DbClusterStatus
    from gspylib.common.ErrorCode import ErrorCode
except ImportError as ie:
    sys.exit("[GAUSS-52200] : Unable to import module: %s." % str(ie))


class OMCommand():
    """
    Descript command of om
    """

    def __init__(self):
        '''
        Constructor
        '''

    @staticmethod
    def getLocalScript(script):
        """
        function: get local script by GPHOME
        input : script, path
        output: path
        """
        Current_Path = os.path.dirname(os.path.realpath(__file__))
        LocalScript = {
            "Local_Cgroup_Remount": os.path.normpath(Current_Path + "/../../local/CgroupRemount.py"),
            "Local_Backup": os.path.normpath(Current_Path + "/../../local/Backup.py"),
            "Local_Query": os.path.normpath(Current_Path + "/../../local/LocalQuery.py"),
            "Local_Check_Config": os.path.normpath(Current_Path + "/../../local/CheckConfig.py"),
            "Local_Check_Install": os.path.normpath(Current_Path + "/../../local/CheckInstall.py"),
            "Local_Check_Uninstall": os.path.normpath(Current_Path + "/../../local/CheckUninstall.py"),
            "Local_Clean_Instance": os.path.normpath(Current_Path + "/../../local/CleanInstance.py"),
            "Local_Check_Upgrade": os.path.normpath(Current_Path + "/../../local/CheckUpgrade.py"),
            "Local_Clean_OsUser": os.path.normpath(Current_Path + "/../../local/CleanOsUser.py"),
            "Local_Config_Hba": os.path.normpath(Current_Path + "/../../local/ConfigHba.py"),
            "Local_Config_Instance": os.path.normpath(Current_Path + "/../../local/ConfigInstance.py"),
            "Local_Init_Instance": os.path.normpath(Current_Path + "/../../local/InitInstance.py"),
            "Local_Install": os.path.normpath(Current_Path + "/../../local/Install.py"),
            "Local_Replace_Config": os.path.normpath(Current_Path + "/../../local/ReplaceConfig.py"),
            "Local_Restore": os.path.normpath(Current_Path + "/../../local/Restore.py"),
            "Local_Uninstall": os.path.normpath(Current_Path + "/../../local/Uninstall.py"),
            "Local_Contraction_Config": os.path.normpath(Current_Path + "/../../local/ContractionConfig.py"),
            "Local_Upgrade_Utility": os.path.normpath(Current_Path + "/../../local/UpgradeUtility.py"),
            "Local_Dilatation_Config": os.path.normpath(Current_Path + "/../../local/DilatationConfig.py"),
            "Local_PreInstall": os.path.normpath(Current_Path + "/../../local/PreInstallUtility.py"),
            "Local_Check_PreInstall": os.path.normpath(Current_Path + "/../../local/CheckPreInstall.py"),
            "Local_UnPreInstall": os.path.normpath(Current_Path + "/../../local/UnPreInstallUtility.py"),
            "Local_Roach": os.path.normpath(Current_Path + "/../../local/LocalRoach.py"),
            "Local_CheckRoachRestore": os.path.normpath(Current_Path + "/../../local/CheckRoachRestore.py"),
            "Gauss_UnInstall": os.path.normpath(Current_Path + "/../../gs_uninstall"),
            "Gauss_Backup": os.path.normpath(Current_Path + "/../../gs_backup"),
            "Local_ManageCN": os.path.normpath(Current_Path + "/../../local/ManageCNUtility.py"),
            "Local_CheckOS": os.path.normpath(Current_Path + "/../../local/LocalCheckOS.py"),
            "Local_Check": os.path.normpath(Current_Path + "/../../local/LocalCheck.py"),
            "Gauss_CheckOS": os.path.normpath(Current_Path + "/../../gs_checkos"),
            "Local_ChangeIP_Utility": os.path.normpath(Current_Path + "/../../local/ChangeIPUtility.py"),
            "Gauss_PreInstall": os.path.normpath(Current_Path + "/../../gs_preinstall"),
            "Gauss_Replace": os.path.normpath(Current_Path + "/../../gs_replace"),
            "Gauss_Om": os.path.normpath(Current_Path + "/../../gs_om"),
            "Gauss_HotPatch": os.path.normpath(Current_Path + "/../../local/gs_hotpatch"),
            "UTIL_GAUSS_STAT": os.path.normpath(Current_Path + "/../../gspylib/common/GaussStat.py"),
            "Gauss_Check": os.path.normpath(Current_Path + "/../../gs_check"),
            "Local_Collect": os.path.normpath(Current_Path + "/../../local/LocalCollect.py"),
            "Local_Kerberos": os.path.normpath(Current_Path + "/../../local/KerberosUtility.py"),
            "Local_Execute_Sql": os.path.normpath(Current_Path + "/../../local/ExecuteSql.py"),
            "Local_Cluster_Resize": os.path.normpath(Current_Path + "/../../local/ClusterResizeUtility.py"),
            "Local_Operation_Record": os.path.normpath(Current_Path + "/../../local/LocalOperationRecord.py"),
            "Local_Config_Guc": os.path.normpath(Current_Path + "/../../local/ConfigGuc.py")
        }

        return "python3 '%s'" % LocalScript[script]

    @staticmethod
    def getSetCronCmd(user, appPath):
        """
        function: Set the crontab
        input : user, appPath
        output: cmd
        """
        log_path = DefaultValue.getOMLogPath(DefaultValue.OM_MONITOR_DIR_FILE, "", appPath)
        cronFile = "%s/gauss_cron_%d" % (DefaultValue.getTmpDirFromEnv(), os.getpid())
        cmd = "crontab -l > %s;" % cronFile
        cmd += "sed -i '/\\/bin\\/om_monitor/d' %s; " % cronFile
        cmd += "echo \"*/1 * * * * source /etc/profile;(if [ -f ~/.profile ];then " \
               "source ~/.profile;fi);source ~/.bashrc;nohup %s/bin/om_monitor -L %s >>/dev/null 2>&1 &\" >> %s;" % \
               (appPath, log_path, cronFile)
        cmd += "crontab -u %s %s;service cron restart;" % (user, cronFile)
        cmd += "rm -f %s" % cronFile

        return cmd

    @staticmethod
    def getRemoveCronCmd(user):
        """
        function: get remove crontab command
        input : user
        output: cmd
        """
        cmd = "crontab -u %s -r;service cron restart" % user

        return cmd

    @staticmethod
    def adaptArchiveCommand(localInstDataDir, similarInstDataDir):
        """
        function: Adapt guc parameter 'archive_command' for each new instance.
                  It will be invoked by GaussReplace.py and GaussDilatation.py
        input : localInstDataDir, similarInstDataDir
        output: NA
        """
        GUC_PARAM_PATTERN = "^\\s*archive_command.*=.*$"
        pattern = re.compile(GUC_PARAM_PATTERN)
        fp = None
        archiveParaLine = ""
        archiveDir = "%s/pg_xlog/archive" % localInstDataDir
        try:
            configFile = os.path.join(localInstDataDir, "postgresql.conf")

            fp = open(configFile, 'r')
            resList = fp.readlines()
            fp.close()
            lineNum = 0
            for oneLine in resList:
                lineNum += 1
                # skip blank line
                if (oneLine.strip() == ""):
                    continue
                # skip comment line
                if ((oneLine.strip()).startswith('#')):
                    continue
                # search valid line
                result = pattern.match(oneLine)
                if (result is not None):
                    # have adapt archive_command parameter
                    archiveParaLine = oneLine.replace(similarInstDataDir, localInstDataDir)
                    archiveList = archiveParaLine.split('#')
                    if (len(archiveList) > 0):
                        archiveCommand = archiveList[0]
                    break

            if (archiveParaLine != ""):
                if (archiveParaLine.find("%f") < 0):
                    raise Exception(ErrorCode.GAUSS_500["GAUSS_50009"] +
                                    " The parameter archive command should be set with %%f : %s." % archiveCommand)

                if (archiveParaLine.find("%p") < 0):
                    raise Exception(ErrorCode.GAUSS_500["GAUSS_50009"] +
                                    " The parameter archive command should be set with %%p: %s." % archiveCommand)

                setCmd = "sed -i \"%dc%s\" %s" % (lineNum, archiveParaLine, configFile)
                (status, output) = subprocess.getstatusoutput(setCmd)
                if (status != 0):
                    raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % setCmd + " Error: \n%s" % output)

                if (os.path.exists(archiveDir) and os.path.isdir(archiveDir)):
                    return

                mkDirCmd = "mkdir -p '%s' -m %s" % (archiveDir, DefaultValue.KEY_DIRECTORY_MODE)
                (status, output) = subprocess.getstatusoutput(mkDirCmd)
                if (status != 0):
                    raise Exception(ErrorCode.GAUSS_502["GAUSS_50208"] % archiveDir + " Error: \n%s" % output)
        except Exception as e:
            if (fp):
                fp.close()
            raise Exception(str(e))

    @staticmethod
    def getInstStatusContext(status_report):
        """
        """
        context = "Coordinator State\n"
        context += "    normal         : %d\n" % status_report.cooNormal
        context += "    abnormal       : %d\n" % status_report.cooAbnormal
        context += "GTM State\n"
        context += "    primary        : %d\n" % status_report.gtmPrimary
        context += "    standby        : %d\n" % status_report.gtmStandby
        context += "    abnormal       : %d\n" % status_report.gtmAbnormal
        context += "    down           : %d\n" % status_report.gtmDown
        context += "Datanode State\n"
        context += "    primary        : %d\n" % status_report.dnPrimary
        context += "    standby        : %d\n" % status_report.dnStandby
        context += "    secondary      : %d\n" % status_report.dnDummy
        context += "    building       : %d\n" % status_report.dnBuild
        context += "    abnormal       : %d\n" % status_report.dnAbnormal
        context += "    down           : %d\n" % status_report.dnDown
        return context

    @staticmethod
    def doCheckStaus(user, nodeId, cluster_normal_status=None, expected_redistributing="", is_dn_slave_ha=False):
        """
        function: Check cluster status
        input : user, nodeId, cluster_normal_status, expected_redistributing
        output: status, output
        """
        statusFile = "/home/%s/gauss_check_status_%d.dat" % (user, os.getpid())
        TempfileManagement.removeTempFile(statusFile)
        cmd = ClusterCommand.getQueryStatusCmd(user, 0, statusFile)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            TempfileManagement.removeTempFile(statusFile)
            return (status, output)

        clusterStatus = DbClusterStatus()
        clusterStatus.initFromFile(statusFile)
        TempfileManagement.removeTempFile(statusFile)

        output = ""
        if (nodeId > 0):
            nodeStatus = clusterStatus.getDbNodeStatusById(nodeId)
            if (nodeStatus is None):
                raise Exception(ErrorCode.GAUSS_516["GAUSS_51619"] % nodeId)

            status = 0 if nodeStatus.isNodeHealthy(is_dn_slave_ha) else 1
            statusRep = nodeStatus.getNodeStatusReport()
        else:
            status = 0 if clusterStatus.isAllHealthy(cluster_normal_status) \
                          and (clusterStatus.redistributing == expected_redistributing or
                               expected_redistributing == "") else 1
            statusRep = clusterStatus.getClusterStatusReport()
            output += "cluster_state      : %s\n" % clusterStatus.clusterStatus
            output += "redistributing     : %s\n" % clusterStatus.redistributing
            output += "node_count         : %d\n" % statusRep.nodeCount
        output += OMCommand.getInstStatusContext(statusRep)
        return status, output

    @staticmethod
    def check_status_degraded(user, expect_cluster_status, check_nodes):
        """
        function: Check cluster status whether is degrade.
        input : user, expect_cluster_status, check_nodes
        output: status, output
        """
        cluster_status = OMCommand.getClusterStatus(user)
        if cluster_status is None:
            return 1, "Failed to get cluster status."

        node_status = [dbNode.isNodeHealthy() for dbNode in cluster_status.dbNodes if dbNode.name in check_nodes]
        status = 0 if (cluster_status.clusterStatus in expect_cluster_status and all(node_status)) else 1

        output = ""
        status_rep = cluster_status.getClusterStatusReport()
        output += "cluster_state      : %s\n" % cluster_status.clusterStatus
        output += "redistributing     : %s\n" % cluster_status.redistributing
        output += "node_count         : %d\n" % status_rep.nodeCount
        output += OMCommand.getInstStatusContext(status_rep)
        return status, output

    @staticmethod
    def getClusterStatus(user, isExpandScene=False):
        """
        function: get cluster status
        input : user
        output: clusterStatus
        """
        statusFile = "/home/%s/gauss_check_status_%d.dat" % (user, os.getpid())
        TempfileManagement.removeTempFile(statusFile)
        cmd = ClusterCommand.getQueryStatusCmd(user, 0, statusFile)
        (status, _) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            TempfileManagement.removeTempFile(statusFile)
            return None
        clusterStatus = DbClusterStatus()
        clusterStatus.initFromFile(statusFile, isExpandScene)
        TempfileManagement.removeTempFile(statusFile)
        return clusterStatus

    @staticmethod
    def getClusterDbNodeInfo(clusterUser, xmlFile=""):
        """
        function: get cluster and database node info from static config file
        input : clusterUser, xmlFile
        output: NA
        """
        try:
            clusterInfo = dbClusterInfo()
            if (os.getuid() == 0):
                clusterInfo.initFromXml(xmlFile)
            else:
                clusterInfo.initFromStaticConfig(clusterUser)
            hostName = socket.gethostname()
            dbNodeInfo = clusterInfo.getDbNodeByName(hostName)
            if (dbNodeInfo is None):
                raise Exception(ErrorCode.GAUSS_516["GAUSS_51619"] % hostName)
            return clusterInfo, dbNodeInfo
        except Exception as e:
            raise Exception(str(e))

    @staticmethod
    def checkHostname(nodename):
        """
        function: check host name
        input : NA
        output: NA
        """
        try:
            retry = 1
            cmd = "export LD_LIBRARY_PATH=/lib64:$LD_LIBRARY_PATH;" \
                  "ssh -n %s %s hostname" % (DefaultValue.SSH_OPTION, nodename)
            while True:
                (status, output) = subprocess.getstatusoutput(cmd)
                if (status == 0 and output.find("%s" % nodename) >= 0):
                    break
                if (retry >= 3):
                    raise Exception(ErrorCode.GAUSS_512["GAUSS_51222"] +
                                    " Command: \"%s\". Error: \n%s" % (cmd, output))
                retry += 1
                time.sleep(1)

        except Exception as e:
            raise Exception(str(e))

    @staticmethod
    def checkHostnameMapping(clusterInfo, logFile):
        """
        function: check host name mapping
        input: NA
        output: NA
        """
        nodes = clusterInfo.getClusterNodeNames()
        if (len(nodes) > 0):
            try:
                pool = ThreadPool(DefaultValue.getCpuSet())
                pool.map(OMCommand.checkHostname, nodes)
                pool.close()
                pool.join()
            except Exception as e:
                raise Exception(str(e))
