#!/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  : CheckInstall.py is a utility to install Gauss MPP Database.
#############################################################################
import getopt
import os
import sys
import platform
import math
import pwd

sys.path.append(sys.path[0] + "/../")
from gspylib.common.GaussLog import GaussLog
from gspylib.common.Common import DefaultValue
from gspylib.common.ParameterParsecheck import Parameter
from gspylib.common.DbClusterInfo import dbClusterInfo
from gspylib.common.ErrorCode import ErrorCode
from gspylib.common.LocalBaseOM import LocalBaseOM
from gspylib.threads.parallelTool import parallelTool
from base_utils.os.cmd_util import CmdUtil
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 base_utils.os.user_util import UserUtil
from domain_utils.domain_common.cluster_constants import ClusterConstants


########################################################################
# Global variables define
########################################################################
g_opts = None
INSTALL_FLAG = "2"


########################################################################
class CmdOptions():
    """
    Class: cmdOptions
    """

    def __init__(self):
        """
        function: Constructor
        input : NA
        output: NA
        """
        self.installPath = ""
        self.user = ""
        self.group = ""
        self.userProfile = ""
        self.mpprcFile = ""
        self.clusterConfig = ""
        self.logFile = ""
        self.userInfo = ""

        # DB config parameters
        self.confParameters = []
        self.platformString = platform.system()
        self.logger = None


#######################################################################
# Help context. U:R:oC:v:
########################################################################
def usage():
    """
python3 checkInstall.py is a utility to check Gauss MPP Database install
env.
Usage:
  python3 checkInstall.py --help
  python3 checkInstall.py -U user:group -R installpath [--replace]

Common options:
  -U        the database program and cluster owner
  -R        the database program path
  -C        configure the database configuration file, for more detail
  information see postgresql.conf
  --replace do check install for replace
  --help    show this help, then exit
    """
    print(usage.__doc__)


def parseCommandLine():
    """
    parse command line
    input : NA
    output: NA
    """
    try:
        opts, args = getopt.getopt(sys.argv[1:], "U:R:C:l:X:", ["help"])
    except getopt.GetoptError as e:
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"] % str(e))
    if (len(args) > 0):
        usage()
        GaussLog.exitWithError(
            ErrorCode.GAUSS_500["GAUSS_50000"] % str(args[0]))

    global g_opts
    g_opts = CmdOptions()

    parameter_map = {"-U": g_opts.userInfo, "-R": g_opts.installPath,
                     "-l": g_opts.logFile, "-X": g_opts.clusterConfig}
    parameter_keys = parameter_map.keys()
    for key, value in opts:
        if (key == "--help"):
            usage()
            sys.exit(0)
        elif (key in parameter_keys):
            parameter_map[key] = value
        elif (key == "-C"):
            g_opts.confParameters.append(value)
        else:
            GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"] % value)
        Parameter.checkParaVaild(key, value)

    g_opts.userInfo = parameter_map["-U"]
    g_opts.installPath = parameter_map["-R"]
    if os.path.islink(g_opts.installPath) or not os.path.exists(
            g_opts.installPath):
        versionFile = VersionInfo.get_version_file()
        commitid = VersionInfo.get_version_info(versionFile)[2]
        g_opts.installPath = g_opts.installPath + "_" + commitid
    g_opts.logFile = parameter_map["-l"]
    g_opts.clusterConfig = parameter_map["-X"]


def checkParameter():
    """
    function: 1.check input parameters
              2.check user info
              3.check os user
              4.check log file info
              5.check mpprc file path
              6.check configFile
              7.check install path
    input : NA
    output: NA
    """
    # check user info
    checkUser(g_opts.userInfo)

    # check mpprc file path
    g_opts.mpprcFile = EnvUtil.getMpprcFile()
    checkOSUser()

    # check log file info
    g_opts.logFile = ClusterLog.checkLogFile(g_opts.logFile, g_opts.user, "",
                                             ClusterConstants.LOCAL_LOG_FILE)
    g_opts.logger = GaussLog(g_opts.logFile, "CheckInstall")


    # check configFile
    checkXMLFile()

    # check install path
    checkInstallPath()


def checkUser(userInfo):
    """
    function: check user
    input : userInfo
    output: NA
    """
    if (userInfo == ""):
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50001"] % 'U' + ".")

    strList = userInfo.split(":")
    if (len(strList) != 2):
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50009"])
    g_opts.user = strList[0].strip()
    g_opts.group = strList[1].strip()
    if (g_opts.user == "" or g_opts.group == ""):
        GaussLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50004"] % "U")


def checkOSUser():
    """
    function: 1.use linux commands 'id -gn' get the user's group
              2.check the user's group match with the input group
              3.get user's env file
    input : NA
    output: NA
    """
    try:
        group = UserUtil.getGroupByUser(g_opts.user)
    except Exception as e:
        GaussLog.exitWithError(str(e))
    if (group != g_opts.group):
        GaussLog.exitWithError(ErrorCode.GAUSS_503["GAUSS_50305"])

    # get user env file
    g_opts.userProfile = g_opts.mpprcFile


def checkXMLFile():
    """
    function: check configuration file
             1.check -X parameter
             2.check configuration file exists
             3.check configuration file an absolute path
    input : NA
    output: NA
    """
    if (g_opts.clusterConfig != ""):
        if (not os.path.exists(g_opts.clusterConfig)):
            g_opts.logger.logExit(
                ErrorCode.GAUSS_502["GAUSS_50201"] % g_opts.clusterConfig)
        if (not os.path.isabs(g_opts.clusterConfig)):
            g_opts.logger.logExit(
                ErrorCode.GAUSS_512["GAUSS_51206"] % g_opts.clusterConfig)


def checkPath(path_type_in):
    """
    function: Check the path:
              the path must be composed of letters, numbers,
              underscores, slashes, hyphen, and spaces
    input : path_type_in
    output: NA
    """
    pathLen = len(path_type_in)
    i = 0
    a_ascii = ord('a')
    z_ascii = ord('z')
    A_ascii = ord('A')
    Z_ascii = ord('Z')
    num0_ascii = ord('0')
    num9_ascii = ord('9')
    blank_ascii = ord(' ')
    sep1_ascii = ord('/')
    sep2_ascii = ord('_')
    sep3_ascii = ord('-')
    sep4_ascii = ord('.')
    for i in range(0, pathLen):
        char_check = ord(path_type_in[i])
        if (not (
                a_ascii <= char_check <= z_ascii
                or A_ascii <= char_check <= Z_ascii
                or num0_ascii <= char_check <= num9_ascii
                or char_check == blank_ascii
                or char_check == sep1_ascii
                or char_check == sep2_ascii
                or char_check == sep3_ascii
                or char_check == sep4_ascii)):
            return False
    return True


def checkInstallPath():
    """
    function: check installation path
    input : NA
    output: NA
    """
    if (g_opts.installPath == ""):
        g_opts.logger.logExit(ErrorCode.GAUSS_500["GAUSS_50001"] % 'R' + ".")
    g_opts.installPath = os.path.normpath(g_opts.installPath)
    g_opts.installPath = os.path.realpath(g_opts.installPath)
    if (not os.path.isdir(os.path.realpath(g_opts.installPath))):
        g_opts.logger.logExit(
            ErrorCode.GAUSS_502["GAUSS_50201"] % g_opts.installPath)
    if (not checkPath(g_opts.installPath)):
        g_opts.logger.logExit(ErrorCode.GAUSS_512["GAUSS_51235"] %
                              g_opts.installPath + " The path must be "
                                                   "composed of"
                                                   "letters, numbers,"
                                                   "underscores,"
                                                   "slashes, hyphen, "
                                                   "and spaces."
                              )

    g_opts.logger.debug(
        "Using installation program path: %s." % g_opts.installPath)
    g_opts.logger.debug("Using set configuration file parameters: %s." % str(
        g_opts.confParameters))


def checkOldInstallStatus():
    """
    function: Check old database install.
              If this user have old install, report error and exit.
    input : NA
    output: NA
    """
    g_opts.logger.log("Checking old installation.")
    # Check $GAUSS_ENV.
    try:
        gauss_ENV = EnvUtil.getEnvironmentParameterValue("GAUSS_ENV",
                                                              g_opts.user)
        if (str(gauss_ENV) == str(INSTALL_FLAG)):
            g_opts.logger.logExit(ErrorCode.GAUSS_518["GAUSS_51806"])
    except Exception as ex:
        g_opts.logger.logExit(str(ex))
    g_opts.logger.log("Successfully checked old installation.")


def checkSHA256():
    """
    function: Check the sha256 number for database install binary file.
    input : NA
    output: NA
    """
    g_opts.logger.log("Checking SHA256.")
    try:
        PackageInfo.checkPackageOS()
    except Exception as e:
        g_opts.logger.logExit(str(e))
    g_opts.logger.log("Successfully checked SHA256.")


def getFileInfo(fileName):
    """
    function:
    input : filename
    output: file context
    """
    res = FileUtil.readFile(fileName)
    if (len(res) != 1):
        raise Exception(ErrorCode.GAUSS_502["GAUSS_50204"] % fileName)
    return res[0].strip()


def checkOSKernel():
    """
    function: Check OS kernel parameters: share memory size and semaphore.(
    postgresql.conf/gtm.conf)
              1.check shared_buffers
              2.check sem
    input : NA
    output: NA
    """
    g_opts.logger.log("Checking kernel parameters.")
    # GB MB kB
    GB = 1 * 1024 * 1024 * 1024
    MB = 1 * 1024 * 1024
    kB = 1 * 1024
    shared_buffers = 1 * GB
    max_connections = 800

    for item in g_opts.confParameters:
        tmp = item.strip()
        listname = tmp.split("=")
        try:
            if (((listname[0].lower() > "shared_buffers") - (
                    listname[0].lower() < "shared_buffers")) == 0):
                if listname[1][0:-2].isdigit() and (
                        (listname[1][-2:] > "GB") - (
                        listname[1][-2:] < "GB")) == 0:
                    shared_buffers = int(listname[1][0:-2]) * GB
                if listname[1][0:-2].isdigit() and (
                        (listname[1][-2:] > "MB") - (
                        listname[1][-2:] < "MB")) == 0:
                    shared_buffers = int(listname[1][0:-2]) * MB
                if listname[1][0:-2].isdigit() and (
                        (listname[1][-2:] > "kB") - (
                        listname[1][-2:] < "kB")) == 0:
                    shared_buffers = int(listname[1][0:-2]) * kB
                if listname[1][0:-1].isdigit() and (
                        (listname[1][-2:] > "B") - (
                        listname[1][-2:] < "B")) == 0:
                    shared_buffers = int(listname[1][0:-1])
            if (((listname[0].lower() > "max_connections") - (
                    listname[0].lower() < "max_connections")) == 0):
                if listname[1].isdigit():
                    max_connections = int(listname[1])
        except ValueError as ex:
            g_opts.logger.logExit(ErrorCode.GAUSS_500[
                                      "GAUSS_50010"] % "kernel" +
                                  "Error:\n%s" % str(
                ex))

    # check shared_buffers
    if (shared_buffers < 128 * kB):
        g_opts.logger.logExit(
            ErrorCode.GAUSS_504["GAUSS_50400"] % ("Shared_buffers", "128KB"))

    try:
        shmaxFile = "/proc/sys/kernel/shmmax"
        shmallFile = "/proc/sys/kernel/shmall"
        shmmax = getFileInfo(shmaxFile)
        shmall = getFileInfo(shmallFile)
        PAGESIZE = CmdUtil.getSysConfiguration()
        if (shared_buffers > int(shmmax)):
            g_opts.logger.logExit(ErrorCode.GAUSS_505["GAUSS_50501"])
        if (shared_buffers > int(shmall) * int(PAGESIZE)):
            g_opts.logger.logExit(ErrorCode.GAUSS_504["GAUSS_50401"] % (
            "Shared_buffers", "shmall*PAGESIZE"))
    except ValueError as ex:
        g_opts.logger.logExit(str(ex))

    try:
        semFile = "/proc/sys/kernel/sem"
        semList = getFileInfo(semFile)
        paramList = semList.split("\t")
        if (int(paramList[0]) < 17):
            g_opts.logger.logExit(
                ErrorCode.GAUSS_524["GAUSS_52401"] % ("SEMMSL", "SEMMSL") +
                paramList[0] + ". Please check it.")
        if (int(paramList[3]) < math.ceil((max_connections + 150) // 16)):
            g_opts.logger.logExit(
                ErrorCode.GAUSS_524["GAUSS_52401"] % ("SEMMNI", "SEMMNI") +
                paramList[3] + ". Please check it.")
        if (int(paramList[1]) < math.ceil((max_connections + 150) // 16) * 17):
            g_opts.logger.logExit(
                ErrorCode.GAUSS_524["GAUSS_52401"] % ("SEMMNS", "SEMMNS") +
                paramList[1] + ". Please check it.")
    except ValueError as ex:
        g_opts.logger.logExit(str(ex))
    g_opts.logger.log("Successfully checked kernel parameters.")


def checkInstallDir():
    """
    function: Check database program file install directory size.
              The free space size should not smaller than 100M.
              1.check if install path exists
              2.check install path is empty or not
              3.check install path uasge
    input : NA
    output: NA
    """
    g_opts.logger.log("Checking directory.")

    # check if install path exists
    if (not os.path.exists(g_opts.installPath)):
        g_opts.logger.logExit(ErrorCode.GAUSS_502[
                                  "GAUSS_50201"] % g_opts.installPath +
                              "\nPlease create it first.")

    # check install path is empty or not.
    installFileList = os.listdir(g_opts.installPath)
    for oneFile in installFileList:
        if (oneFile == "full_upgrade_bak" and os.path.isdir(
                "%s/%s" % (g_opts.installPath, oneFile))):
            continue
        elif (oneFile == "lib" and os.path.isdir(
                "%s/%s" % (g_opts.installPath, oneFile))):
            libList = os.listdir("%s/%s" % (g_opts.installPath, oneFile))
            if (len(libList) == 1 and libList[
                0] == "libsimsearch" and os.path.isdir(
                    "%s/%s/%s" % (g_opts.installPath, oneFile, libList[0]))):
                continue
            else:
                g_opts.logger.logExit(
                    ErrorCode.GAUSS_502["GAUSS_50202"] % g_opts.installPath)
        elif (oneFile == "bin" and os.path.isdir(
                "%s/%s" % (g_opts.installPath, oneFile))):
            binFieList = os.listdir("%s/%s" % (g_opts.installPath, oneFile))
            for binFie in binFieList:
                if (binFie.find("cluster_static_config") < 0):
                    g_opts.logger.logExit(ErrorCode.GAUSS_502[
                                              "GAUSS_50202"] %
                                          g_opts.installPath)
        elif (oneFile == "secbox" and os.path.isdir(
                "%s/%s" % (g_opts.installPath, oneFile))):
            continue
        else:
            g_opts.logger.logExit(
                ErrorCode.GAUSS_502["GAUSS_50202"] % g_opts.installPath)

    # check install path uasge
    vfs = os.statvfs(g_opts.installPath)
    availableSize = vfs.f_bavail * vfs.f_bsize // (1024 * 1024)

    g_opts.logger.log(
        "Database program installation path available size %sM." % str(
            availableSize))
    if (availableSize < 100):
        g_opts.logger.logExit(
            ErrorCode.GAUSS_504["GAUSS_50400"] % (g_opts.installPath, "100M"))

    g_opts.logger.log("Successfully checked directory.")


class CheckInstall(LocalBaseOM):
    """
    Class: check install
    """

    def __init__(self, logFile, user, clusterConf, dwsMode=False):
        """
        function: Constructor
        input : logFile, user, clusterConf, dwsMode
        output: NA
        """
        LocalBaseOM.__init__(self, logFile, user, clusterConf, dwsMode)
        if (self.clusterConfig == ""):
            # Read config from static config file
            self.readConfigInfo()
        else:
            self.clusterInfo = dbClusterInfo()
            self.clusterInfo.initFromXml(self.clusterConfig)
            hostName = NetUtil.GetHostIpOrName()
            self.dbNodeInfo = self.clusterInfo.getDbNodeByName(hostName)
            if (self.dbNodeInfo is None):
                self.logger.logExit(
                    ErrorCode.GAUSS_516["GAUSS_51619"] % hostName)
        # get user info
        self.getUserInfo()
        if (user != "" and self.user != user.strip()):
            self.logger.debug("User parameter : %s." % user)
            self.logger.logExit(ErrorCode.GAUSS_503["GAUSS_50315"] % (
            self.user, self.clusterInfo.appPath))
        # init every component
        self.initComponent()

    def checkPortAndIp(self):
        """
        function: Check instance port and IP
        input : NA
        output: NA
        """
        self.logger.log("Checking instance port and IP.")
        components = self.etcdCons + self.cmCons + self.gtmCons \
                     + self.cnCons + self.dnCons
        try:
            # config instance in paralle
            parallelTool.parallelExecute(self.checkIpAndPort, components)
        except Exception as e:
            self.logger.logExit(str(e))
        self.logger.log("Successfully checked instance port and IP.")

    def checkIpAndPort(self, component):
        """
        function: Check instance port and IP for per component
        input : component
        output: NA
        """
        component.perCheck()

    def checkPreEnv(self):
        """
        function: Check if LD path and path in preinstall had been changed.
        input : NA
        output: NA
        """
        g_opts.logger.log("Checking preinstall enviroment value.")
        # Check $GAUSS_ENV.
        try:
            # get mpp file by env parameter MPPDB_ENV_SEPARATE_PATH
            mpprcFile = EnvUtil.getEnv(DefaultValue.MPPRC_FILE_ENV)
            if (mpprcFile != "" and mpprcFile is not None):
                userProfile = mpprcFile
                if (not os.path.isabs(userProfile)):
                    raise Exception(
                        ErrorCode.GAUSS_512["GAUSS_51206"] % userProfile)
                if (not os.path.exists(userProfile)):
                    raise Exception(
                        ErrorCode.GAUSS_502["GAUSS_50201"] % userProfile)
            else:
                userpath = pwd.getpwnam(self.user).pw_dir
                userProfile = os.path.join(userpath, ".bashrc")
            reEnvList = FileUtil.readFile(userProfile)
            checkList = [
                "export PATH=$GPHOME/script/gspylib/pssh/bin:$GPHOME/script"
                ":$PATH",
                "export LD_LIBRARY_PATH=$GPHOME/lib:$LD_LIBRARY_PATH"]
            for check in checkList:
                if (check not in reEnvList and (
                        check + '\n') not in reEnvList):
                    self.logger.logExit(
                        ErrorCode.GAUSS_518["GAUSS_51802"] % check)
        except Exception as e:
            g_opts.logger.logExit(str(e))
        g_opts.logger.log("Successfully checked preinstall enviroment value.")


if __name__ == '__main__':
    ###################################################################
    # check install
    ###################################################################
    """
    function:   Check all kinds of environment. It includes:
                1. Input parameters.
                2. OS version .
                3. If it has a old install.
                4. OS kernel parameters.
                5. Install directory size and stauts.
                6. Security.
                7. Binary file integrity verify.
    input : NA
    output: NA
    """
    try:
        # parse and check input parameters
        parseCommandLine()
        checkParameter()
        # Check whether the old database installed
        checkOldInstallStatus()
        # Check the sha256 number for database install binary file
        checkSHA256()

        if (g_opts.platformString == "Linux"):
            # Check OS kernel parameters: share memory size
            # and semaphore.(postgresql.conf/gtm.conf)
            checkOSKernel()
            # Check database program file install directory size
            checkInstallDir()

        if (g_opts.clusterConfig != ""):
            # Check instance port and IP
            checker = CheckInstall(g_opts.logFile, g_opts.user,
                                   g_opts.clusterConfig)
            checker.checkPortAndIp()
            checker.checkPreEnv()

        g_opts.logger.closeLog()
    except Exception as e:
        GaussLog.exitWithError(str(e))

    sys.exit(0)
