#!/usr/bin/env python3
# -*- coding:utf-8 -*-
#############################################################################
# Copyright (c) 2020 Huawei Technologies Co.,Ltd.
#
# openGauss is licensed under Mulan PSL v2.
# You can use this software according to the terms
# and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#
#          http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS,
# WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
# ----------------------------------------------------------------------------
# Description  : CheckUpgrade.py is a utility to check the env before upgrade.
#############################################################################
import getopt
import sys
import os
import subprocess
import pwd
import traceback

sys.path.append(sys.path[0] + "/../")
from gspylib.common.GaussLog import GaussLog
from gspylib.common.DbClusterInfo import dbClusterInfo
from gspylib.common.ParameterParsecheck import Parameter
from gspylib.common.ErrorCode import ErrorCode
import impl.upgrade.UpgradeConst as Const
from domain_utils.cluster_file.cluster_dir import ClusterDir
from domain_utils.cluster_file.cluster_log import ClusterLog
from base_utils.os.env_util import EnvUtil
from base_utils.os.file_util import FileUtil
from domain_utils.cluster_file.package_info import PackageInfo
from domain_utils.cluster_file.version_info import VersionInfo
from base_utils.os.net_util import NetUtil
from domain_utils.domain_common.cluster_constants import ClusterConstants
from base_utils.common.constantsbase import ConstantsBase

INSTANCE_TYPE_UNDEFINED = -1
MASTER_INSTANCE = 0
STANDBY_INSTANCE = 1
DUMMY_STANDBY_INSTANCE = 2

#############################################################################
# Global variables
#############################################################################
g_logger = None
g_clusterInfo = None


class CmdOptions():
    """
    Class for defining some cmd options
    """

    def __init__(self):
        self.action = ""
        # the current old appPath
        self.appPath = ""
        self.user = ""
        self.logFile = ""
        self.xmlFile = ""
        self.upgrade_version = ""
        self.newAppPath = ""


class CheckUpgrade():
    """
    Class to Check application setting for upgrade
    """

    def __init__(self, appPath, action, newAppPath):
        '''
        Constructor
        '''
        self.appPath = appPath
        self.action = action
        self.newAppPath = newAppPath

    def run(self):
        """
        function: Check upgrade environment
        input: NA
        output: NA
        """
        self.__checkSHA256()
        self.__checkAppPath()
        self.__checkDataDir()
        if self.action == Const.ACTION_INPLACE_UPGRADE:
            self.__checkBackupDir()
            self.__checkAppVersion()
            self.__backupDbClusterInfo()

    def __checkAppPath(self):
        """
        function: check app path
        input: NA
        output: NA
        """
        if not os.path.isdir(self.appPath):
            g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50201"]
                             % self.appPath)

        static_config = "%s/bin/cluster_static_config" % self.appPath
        if not os.path.exists(static_config):
            g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50201"]
                             % static_config)
        if not os.path.isfile(static_config):
            g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50210"]
                             % static_config)

        if not os.path.isdir(self.newAppPath):
            g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50201"]
                             % self.newAppPath)
        if os.path.samefile(self.newAppPath, self.appPath):
            g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50233"]
                             % ("install path", "$GAUSSHOME"))

        # check if the current app path is correct size,
        # there should be no personal data
        cmd = "du -hms %s | awk '{print $1}'" % os.path.realpath(self.appPath)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            g_logger.logExit(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd
                             + " ERROR: %s" % str(output))
        appSize = output

        cmd = "du -hms %s/lib/postgresql/pg_plugin | awk '{print $1}'" \
              % os.path.realpath(self.appPath)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            g_logger.logExit(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd +
                             " ERROR: %s" % str(output))
        pluginSize = output
        if int(appSize) - int(pluginSize) > Const.MAX_APP_SIZE:
            g_logger.logExit(ErrorCode.GAUSS_504["GAUSS_50401"]
                             % (self.appPath, "%dM" % Const.MAX_APP_SIZE) +
                             "\nThere may be personal data in path %s,"
                             " please move your data to other directory"
                             % self.appPath)

    def __checkAppVersion(self):
        """
        function: Check version
        input: NA
        output: NA
        """
        # grey upgrade no need do this check
        curVer = VersionInfo.getAppVersion(self.appPath)
        if (curVer == ""):
            g_logger.logExit(ErrorCode.GAUSS_516["GAUSS_51623"])

        gaussHome = EnvUtil.getEnvironmentParameterValue("GAUSSHOME",
                                                              g_opts.user)
        if not gaussHome:
            g_logger.logExit(ErrorCode.GAUSS_518["GAUSS_51800"]
                             % "$GAUSSHOME")

        gaussdbFile = "%s/bin/gaussdb" % gaussHome
        cmd = "%s --version 2>/dev/null" % (gaussdbFile)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            g_logger.logExit(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd
                             + "Error:\n %s" % output)

    def __checkSHA256(self):
        '''
        function: Check the sha256 of new version
        input: NA
        output: NA
        '''
        try:
            PackageInfo.checkPackageOS()
        except Exception as e:
            g_logger.logExit(str(e))

    def __getTmpDir(self):
        """
        """
        return EnvUtil.getTmpDirFromEnv()

    def __getBackupDir(self):
        """
        """
        return "%s/binary_upgrade" % EnvUtil.getTmpDirFromEnv()

    def __backupDbClusterInfo(self):
        """
        function: backup DbClusterInfo.py and cluster_static_config to temp
                  path
        input: NA
        output: NA
        """
        commonStaticConfigFile = "%s/bin/cluster_static_config" \
                                 % g_opts.appPath
        commonUpgradeVersionFile = "%s/bin/upgrade_version" % g_opts.appPath
        commonDbClusterInfoModule = \
            "%s/bin/script/gspylib/common/DbClusterInfo.py" % g_opts.appPath

        bakPath = self.__getTmpDir()

        # backup DbClusterInfo.py
        oldDbClusterInfoModule = "%s/OldDbClusterInfo.py" % bakPath
        cmd = "cp -p '%s'  '%s'" % (commonDbClusterInfoModule,
                                    oldDbClusterInfoModule)
        cmd += " && cp -rp '%s/bin/script/' '%s'" % (g_opts.appPath, bakPath)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            g_logger.logExit(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd
                             + "\nOutput:%s" % output)

        # backup cluster_static_config
        cmd = "cp -p '%s' '%s'/" % (commonStaticConfigFile, bakPath)
        (status, output) = subprocess.getstatusoutput(cmd)
        if (status != 0):
            g_logger.logExit(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd
                             + "\nOutput:%s" % output)

        upgradeBakPath = self.__getBackupDir()
        try:
            # backup upgrade_version
            oldUpgradeVersionFile = "%s/old_upgrade_version" % upgradeBakPath
            cmd = "cp -p %s  %s" % (commonUpgradeVersionFile,
                                    oldUpgradeVersionFile)
            (status, output) = subprocess.getstatusoutput(cmd)
            if (status != 0):
                raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd
                                + "\nOutput:%s" % output)

        except Exception as e:
            g_logger.debug("Backup failed.ERROR:%s\nClean backup path."
                           % str(e))
            if (os.path.isdir(upgradeBakPath)):
                FileUtil.removeDirectory(upgradeBakPath)

    def __checkBackupDir(self):
        """
        for binary upgrade, Check if backup dir exists, it may be not empty,
         because in the second time, we may have
        file record app dir and record node app
        INPUT:NA
        OUTPUT:NA
        HIDEN:
        1.paths need to be baked
        2.100M
        PRECONDITION:
        POSTCONDITION:
        2.for binary upgrade, bak dir has been ready
        TEST:
        Pseudocode:
        """
        binaryBakDir = "%s/binary_upgrade" % EnvUtil.getTmpDirFromEnv()
        if not os.path.isdir(binaryBakDir):
            os.makedirs(binaryBakDir, ConstantsBase.KEY_DIRECTORY_PERMISSION)

        vfs = os.statvfs(binaryBakDir)
        availableSize = vfs.f_bavail * vfs.f_bsize / (1024 * 1024)
        g_logger.debug("The available size of backup directory: %d M."
                       % availableSize)
        if(availableSize < Const.MAX_APP_SIZE):
            g_logger.logExit(ErrorCode.GAUSS_504["GAUSS_50400"]
                             % ("BakDir", "%dM" % Const.MAX_APP_SIZE))

    def __checkDataDir(self):
        """
        function: check data directory access rights.
        input: NA
        output:NA
        """
        g_logger.debug("Checking data directory access rights.")

        instDirs = self.getNodeDirs()
        for path in instDirs:
            if (not os.path.exists(path)):
                g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50201"] % path)
            if not FileUtil.checkDirWriteable(path):
                g_logger.logExit(ErrorCode.GAUSS_502["GAUSS_50205"] % path)

        g_logger.debug("Successfully to check data directory access rights.")

    def getNodeDirs(self, pathType=""):
        """
        function: get the install path and data path of cluster
                  1. collect install path
                  2. collect data path
                  3. collect tablespc path
                  4. remove the same items
        input : pathType
        output : tempPaths
        """
        localHost = NetUtil.GetHostIpOrName()
        dbNode = g_clusterInfo.getDbNodeByName(localHost)
        if not dbNode:
            g_logger.logExit(ErrorCode.GAUSS_512["GAUSS_51209"]
                             % ("NODE", localHost))
        newNodePaths = []
        DnInfos = []
        tablespacePaths = []

        # collect install path
        newNodePaths.append(g_clusterInfo.appPath)

        for instance in dbNode.datanodes:
            newNodePaths.append(instance.datadir)
            if (instance.instanceType != DUMMY_STANDBY_INSTANCE):
                DnInfos.append(instance.datadir)
                if ('ssdDir' in dir(instance)):
                    if (len(instance.ssdDir) != 0):
                        newNodePaths.append(instance.ssdDir)

        # collect tablespc path
        for instanceDir in DnInfos:
            if (not os.path.exists("%s/pg_tblspc" % instanceDir)):
                g_logger.debug("%s/pg_tblspc does not exist." % instanceDir)
                continue
            fileList = os.listdir("%s/pg_tblspc" % instanceDir)
            if (len(fileList)):
                for filename in fileList:
                    if (os.path.islink("%s/pg_tblspc/%s"
                                       % (instanceDir, filename))):
                        linkDir = os.readlink("%s/pg_tblspc/%s"
                                              % (instanceDir, filename))
                        if (os.path.isdir(linkDir)):
                            tablespacePaths.append(linkDir)
                        else:
                            g_logger.debug("%s is not a link directory."
                                           % filename)
                    else:
                        g_logger.debug("%s is not a link file." % filename)
            else:
                g_logger.debug("%s/pg_tblspc is empty." % instanceDir)

        if (pathType == "tablespace"):
            tempPaths = tablespacePaths
        else:
            tempPaths = newNodePaths + tablespacePaths

        # remove the same items
        tempPaths = list(set(tempPaths))

        return tempPaths


def usage():
    """
Usage:
  python3 CheckUpgrade.py -t action -R installpath -N newClusterInstallPath
   [-U user] [-l log]
Common options:
  -t                                the type of action
  -l                                the path of log file
  --help                            show this help, then exit
Options for big version upgrade check
  -U                                the user of old cluster
  -X                                path of the XML configuration file
Options for upgrade check
  -R                                the install path of old cluster
  -N                                the install path of new cluster
  -v                                backup upgrade version
    """
    print(usage.__doc__)


def parseCommandLine():
    """
    function: Parse command line and save to global variable
    input: NA
    output: NA
    """
    try:
        opts, args = getopt.getopt(sys.argv[1:], "t:R:U:v:l:X:N:", ["help"])
    except Exception as e:
        usage()
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"] % str(e))

    if(len(args) > 0):
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"]
                               % str(args[0]))

    for (key, value) in opts:
        if (key == "--help"):
            usage()
            sys.exit(0)
        elif (key == "-t"):
            g_opts.action = value
        elif (key == "-R"):
            g_opts.appPath = value
        elif (key == "-U"):
            g_opts.user = value
        elif (key == "-l"):
            g_opts.logFile = os.path.realpath(value)
        elif (key == "-X"):
            g_opts.xmlFile = os.path.realpath(value)
        elif (key == "-v"):
            g_opts.upgrade_version = value
        elif (key == "-N"):
            g_opts.newAppPath = value
        else:
            GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"] % key)

        Parameter.checkParaVaild(key, value)


def checkParameter():
    """
    function: Parse command line and save to global variable
    input: NA
    output: NA
    """
    # only check need parameter, just ignore no need parameter
    if (g_opts.user == ""):
        g_opts.user = pwd.getpwuid(os.getuid()).pw_name

    if (g_opts.logFile == ""):
        g_opts.logFile = ClusterLog.getOMLogPath(
            ClusterConstants.LOCAL_LOG_FILE, "", g_opts.appPath, "")

    if g_opts.action in [Const.ACTION_LARGE_UPGRADE,
                         Const.ACTION_SMALL_UPGRADE,
                         Const.ACTION_INPLACE_UPGRADE]:
        if (g_opts.appPath == ""):
            GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50001"]
                                   % "R" + ".")

    elif(g_opts.action == Const.ACTION_CHECK_VERSION):
        if(g_opts.upgrade_version == ""):
            GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50001"]
                                   % "v" + ".")
    else:
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50004"] % 't'
                               + " Value: %s." % g_opts.action)


def checkVersion():
    """
    function: check version information
    input: NA
    output: NA
    """
    g_logger.debug("Checking version information.")
    gaussHome = ClusterDir.getInstallDir(g_opts.user)
    if gaussHome == "":
        raise Exception(ErrorCode.GAUSS_518["GAUSS_51800"] % "$GAUSSHOME")
    localPostgresVersion = \
        VersionInfo.getAppBVersion(os.path.realpath(gaussHome))
    if (localPostgresVersion.find(g_opts.upgrade_version) > 0):
        g_logger.debug("Successfully checked version information.")
    else:
        raise Exception(ErrorCode.GAUSS_529["GAUSS_52935"])


class OldVersionModules():
    """
    Class for providing some functions to apply old version cluster
    """

    def __init__(self):
        '''
        Constructor
        '''
        self.oldDbClusterInfoModule = None
        self.oldDbClusterStatusModule = None


def initGlobalInfos():
    """
    function: init global infos
    input: NA
    output: NA
    """
    global g_logger
    global g_clusterInfo
    g_logger = GaussLog(g_opts.logFile, "CheckUpgrade")
    try:
        if g_opts.action in [Const.ACTION_CHECK_VERSION]:
            g_logger.log("No need to init cluster info under action %s"
                         % g_opts.action)
            return
        g_clusterInfo = dbClusterInfo()
        if g_opts.xmlFile == "" or not os.path.exists(g_opts.xmlFile):
            if g_opts.appPath == "" or not os.path.exists(g_opts.appPath):
                raise Exception(ErrorCode.GAUSS_500["GAUSS_50001"] % "R")
            staticConfigFile = "%s/bin/cluster_static_config" % g_opts.appPath
            g_clusterInfo.initFromStaticConfig(g_opts.user, staticConfigFile)
        else:
            g_clusterInfo.initFromXml(g_opts.xmlFile)
    except Exception as e:
        g_logger.log(traceback.format_exc())
        g_logger.logExit(str(e))


if __name__ == '__main__':
    """
    main function
    1. parse command
    2. check other parameter
    3. init global infos
    4. check version information
    5. Check application setting for upgrade
    """
    try:
        g_opts = CmdOptions()
        # 1. parse command
        parseCommandLine()
        # 2. check other parameter
        checkParameter()
        # 3. init global infos
        initGlobalInfos()
    except Exception as e:
        GaussLog.exitWithError(str(e) + traceback.format_exc())

    try:
        # 4. check version information
        if(g_opts.action == Const.ACTION_CHECK_VERSION):
            checkVersion()
        else:
            # 5. Check application setting for upgrade
            g_logger.log("Checking upgraded environment.")
            checker = CheckUpgrade(g_opts.appPath, g_opts.action,
                                   g_opts.newAppPath)
            checker.run()
            g_logger.log("Successfully checked upgraded environment.")
        g_logger.closeLog()
    except Exception as e:
        g_logger.log(traceback.format_exc())
        g_logger.logExit(str(e))

    sys.exit(0)
