#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
import re

from gspylib.common.ErrorCode import ErrorCode
from gspylib.common.VersionInfo import VersionInfo
from gspylib.common.common.default_value import DefaultValue
from gspylib.hardware.gsmemory import g_memory
from gspylib.os.gsOSlib import g_OSlib

A1 = "A1"  # OS version status
A2 = "A2"  # Kernel version status
A3 = "A3"  # Unicode status
A4 = "A4"  # Time zone status
A5 = "A5"  # Swap memory status
A6 = "A6"  # System control parameters status
A7 = "A7"  # File system configuration status
A8 = "A8"  # Disk configuration status
A9 = "A9"  # Pre-read block size status
A10 = "A10"  # IO scheduler status
A11 = "A11"  # Network card configuration status
A12 = "A12"  # Time consistency status
A13 = "A13"  # Firewall service status
A14 = "A14"  # THP service status
B1 = "B1"  # Set system control parameters
B2 = "B2"  # Set file system configuration value
B3 = "B3"  # Set pre-read block size value
B4 = "B4"  # Set IO scheduler value
B5 = "B5"  # Set network card configuration value
B6 = "B6"  # Set THP service

CHECK_ITEMNUMLIST = [A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14]
SET_ITEMNUMLIST = [B1, B2, B3, B4, B5, B6]
CHECK_ITEMS = {
    A1: ['Checking items', '[ OS version status ]', 'Normal', 'OK', 'OK'],
    A2: ['Checking items', '[ Kernel version status ]', 'Normal', 'OK', 'OK'],
    A3: ['Checking items', '[ Unicode status ]', 'Normal', 'OK', 'OK'],
    A4: ['Checking items', '[ Time zone status ]', 'Normal', 'OK', 'OK'],
    A5: ['Checking items', '[ Swap memory status ]', 'Normal', 'OK', 'OK'],
    A6: ['Checking items', '[ System control parameters status ]', 'Normal', 'OK', 'OK'],
    A7: ['Checking items', '[ File system configuration status ]', 'Normal', 'OK', 'OK'],
    A8: ['Checking items', '[ Disk configuration status ]', 'Normal', 'OK', 'OK'],
    A9: ['Checking items', '[ Pre-read block size status ]', 'Normal', 'OK', 'OK'],
    A10: ['Checking items', '[ IO scheduler status ]', 'Normal', 'OK', 'OK'],
    A11: ['Checking items', '[ Network card configuration status ]', 'Normal', 'OK', 'OK'],
    A12: ['Checking items', '[ Time consistency status ]', 'Normal', 'OK', 'OK'],
    A13: ['Checking items', '[ Firewall service status ]', 'Normal', 'OK', 'OK'],
    A14: ['Checking items', '[ THP service status ]', 'Normal', 'OK', 'OK'],
    B1: ['Setting items', '[ Set system control parameters ]', 'Normal', 'OK', 'OK'],
    B2: ['Setting items', '[ Set file system configuration value ]', 'Normal', 'OK', 'OK'],
    B3: ['Setting items', '[ Set pre-read block size value ]', 'Normal', 'OK', 'OK'],
    B4: ['Setting items', '[ Set IO scheduler value ]', 'Normal', 'OK', 'OK'],
    B5: ['Setting items', '[ Set network card configuration value ]', 'Normal', 'OK', 'OK'],
    B6: ['Setting items', '[ Set THP service ]', 'Normal', 'OK', 'OK']}

ACTION_CHECK_OS_VERSION = "Check_OS_Version"
ACTION_CHECK_KERNEL_VERSION = "Check_Kernel_Version"
ACTION_CHECK_UNICODE = "Check_Unicode"
ACTION_CHECK_TIMEZONE = "Check_TimeZone"
ACTION_CHECK_SYSCTL_PARAMETER = "Check_SysCtl_Parameter"
ACTION_CHECK_DISK_CONFIGURE = "Check_Disk_Configure"
ACTION_CHECK_BLOCKDEV_CONFIGURE = "Check_BlockDev_Configure"
ACTION_CHECK_LOGICAL_BLOCK = "Check_Logical_Block"
ACTION_CHECK_IO_REQUEST = "Check_IO_Request"
ACTION_CHECK_ASYNCHRONOUS_IO_REQUEST = "Check_Asynchronous_IO_Request"
ACTION_CHECK_IO_CONFIGURE = "Check_IO_Configure"
ACTION_CHECK_NETWORK_CONFIGURE = "Check_Network_Configure"
ACTION_CHECK_NETWORK_BOND_MODE = "Check_Network_Bond_Mode"
ACTION_CHECK_SWAP_MEMORY_CONFIGURE = "Check_Swap_Memory_Configure"
ACTION_CHECK_FILESYSTEM_CONFIGURE = "Check_FileSystem_Configure"
ACTION_CHECK_TIME_CONSISTENCY = "Check_Time_Consistency"
ACTION_CHECK_FIREWALL_SERVICE = "Check_Firewall_Service"
ACTION_CHECK_THP_SERVICE = "Check_THP_Service"
ACTION_SET_SYSCTL_PARAMETER = "Set_SysCtl_Parameter"
ACTION_SET_FILESYSTEM_CONFIGURE = "Set_FileSystem_Configure"
ACTION_SET_NETWORK_CONFIGURE = "Set_Network_Configure"
ACTION_SET_THP_SERVICE = "Set_THP_Service"
ACTION_SET_BLOCKDEV_CONFIGURE = "Set_BlockDev_Configure"
ACTION_SET_LOGICAL_BLOCK = "Set_Logical_Block"
ACTION_SET_IO_CONFIGURE = "Set_IO_Configure"
ACTION_SET_IO_REQUEST = "Set_IO_REQUEST"
ACTION_SET_ASYNCHRONOUS_IO_REQUEST = "Set_Asynchronous_IO_Request"
LEVEL_WARNING = "Warning"
LEVEL_ABNORMAL = "Abnormal"
FORMAT = " " * 8
SECTION_SYSCTL = '/etc/sysctl.conf'
SECTION_LIMITS = '/etc/security/limits.conf'
SECTION_BLOCKDEV = 'blockdev'
SECTION_LOGICAL_BLOCK_SIZE = "logical_block_size"
SECTION_NR_REQUEST = "nr_requests"
SECTION_AIO_MAX_NR = "aio-max-nr"
SECTION_SCHEDULER = "scheduler"
SECTION_IF_CONFIG = "/sbin/ifconfig"
SECTION_THP = "transparent_hugepage"

actionItemMap = {
    ACTION_CHECK_SYSCTL_PARAMETER: SECTION_SYSCTL,
    ACTION_CHECK_FILESYSTEM_CONFIGURE: SECTION_LIMITS,
    ACTION_SET_SYSCTL_PARAMETER: SECTION_SYSCTL,
    ACTION_SET_FILESYSTEM_CONFIGURE: SECTION_LIMITS
}

FORCE_SETUP = "force_setup"
NO_SETUP = "no_setup"
OUT_RANGE_SETUP = "out_range_setup"
NEW_ADD_SETUP = "new_add_setup"

LIMITS_MAP = {"open files": "nofile",
              "stack size": "stack",
              "virtual memory": "max",
              "max locked memory": "memlock",
              }


class Logger(object):
    def __init__(self):
        pass

    def log(self, msg):
        pass

    def debug(self, msg):
        pass


class CheckUtils(object):
    def __init__(self, user, logger, cluster_info):
        self.user = user
        self.cluster_info = cluster_info
        self.is_upgrade_preinstall = None
        self.pkg_version = self._get_pkg_version()
        self.logger = logger if logger else Logger()

    ############################################################
    # COMMON INTERFACE : parse check_list.conf.
    ############################################################
    def get_check_list(self, section):
        """
        :param section:
        :return:
        """
        dir_name = os.path.dirname(os.path.realpath(__file__))
        config_file = "%s/../../etc/conf/check_list.conf" % dir_name
        parameter_list = DefaultValue.getConfigFilePara(config_file, section)

        result_dict = {}
        for para_key, para_policy in parameter_list.items():
            para_policy_list = para_policy.split('#')
            if len(para_policy_list) != 2:
                continue

            para_value = para_policy_list[0].strip()
            setup_policy = para_policy_list[1].strip()
            result_dict[para_key] = {"value": para_value}

            setup_policy_list = setup_policy.split('|')
            for setup_policy_item in setup_policy_list:
                spi = setup_policy_item.split(':')
                if len(spi) != 2:
                    continue

                spi_key = spi[0].strip()
                spi_value = spi[1].strip()
                result_dict[para_key][spi_key] = spi_value

        self.logger.debug("Check list policy:%s." % str(result_dict))
        return result_dict

    ############################################################
    # COMMON INTERFACE : check parameter by check policy.
    ############################################################
    def check_parameter(self, para_key, check_policy, current_value):
        """
        :param para_key: must be unique in check_list.conf.
        :param check_policy:
        :param current_value:
        :return: True or False
        """
        if para_key == "vm.min_free_kbytes":
            return self._check_min_free_kbytes(check_policy, current_value)
        elif para_key == "net.ipv4.tcp_mem":
            return self._check_tcp_mem(check_policy, current_value)
        elif 'range' in check_policy:
            return self._simple_range_check(check_policy, current_value)
        else:
            return self._simple_value_check(check_policy, current_value)

    def _simple_range_check(self, check_policy, current_value):
        range_str = check_policy['range'].replace('[', '').replace(']', '')

        min_val, max_val, range_list = 0, 0, []
        if range_str.find('-') >= 0:
            is_list_range = False
            min_val, max_val = range_str.split('-')
            min_val = int(min_val.strip()) if min_val.strip().isdigit() else None
            max_val = int(max_val.strip()) if max_val.strip().isdigit() else None
        else:
            is_list_range = True
            range_list = [i.strip() for i in range_str.split(',')]

        if is_list_range:
            if current_value not in range_list:
                return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['range'])
        else:
            if min_val and max_val and not (min_val <= int(current_value) <= max_val):
                return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['range'])
            elif min_val and not max_val and int(current_value) < min_val:
                return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['range'])
            elif not min_val and max_val and int(current_value) > max_val:
                return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['range'])

        return True, ""

    def _simple_value_check(self, check_policy, current_value):
        if check_policy['value'].split() != current_value.split():
            return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['value'])
        else:
            return True, ""

    ############################################################
    # self defined parameter : including getters and checkers.
    ############################################################
    @staticmethod
    def _get_min_free_kbytes(check_policy):
        total_mem = g_memory.getMemTotalSize() // 1024  # KB
        request_val = int(float(check_policy['value'].replace('%', '').strip()) * total_mem // 100)
        return request_val

    @staticmethod
    def _get_tcp_mem(check_policy):
        val_items = check_policy['value'].replace('%', '').split()
        if len(val_items) != 3:
            raise Exception("Value setup [%s] is invalid." % check_policy['value'])

        try:
            val_items_0 = float(val_items[0].strip())
            val_items_1 = float(val_items[1].strip())
            val_items_2 = float(val_items[2].strip())
        except Exception as _:
            raise Exception("Value setup [%s] is invalid." % check_policy['value'])

        total_mem = g_memory.getMemTotalSize()  # Bytes
        page_size = int(g_OSlib.getSysConfiguration().strip())  # Bytes

        request_val = "%d %d %d" % \
                      (int(total_mem * val_items_0 / 100 / page_size),
                       int(total_mem * val_items_1 / 100 / page_size),
                       int(total_mem * val_items_2 / 100 / page_size))

        return request_val

    def _check_min_free_kbytes(self, check_policy, current_value):
        request_val = self._get_min_free_kbytes(check_policy)
        if not (request_val * 0.9 <= int(current_value) <= request_val * 1.1):
            return False, "Current:[%s],Request:[%s]" % (current_value, request_val)
        else:
            return True, ""

    def _check_tcp_mem(self, check_policy, current_value):
        curren_value0, curren_value1, curren_value2 = current_value.split()
        curren_value0, curren_value1, curren_value2 = int(curren_value0), int(curren_value1), int(curren_value2)
        if "range" in check_policy:
            range_items = check_policy['range'].replace('[', '').replace(']', '').replace('%', '').split(',')
            if len(range_items) != 3:
                return False, "Range setup [%s] is invalid." % check_policy['range']

            range0 = range_items[0].split('-')
            range1 = range_items[1].split('-')
            range2 = range_items[2].split('-')
            if len(range0) != 2 or len(range1) != 2 or len(range2) != 2:
                return False, "Range setup [%s] is invalid." % check_policy['range']

            total_mem = g_memory.getMemTotalSize()  # Bytes
            page_size = int(g_OSlib.getSysConfiguration().strip())  # Bytes
            range0_val0 = int(total_mem * float(range0[0].strip()) / 100 / page_size)
            range0_val1 = int(total_mem * float(range0[1].strip()) / 100 / page_size)
            range1_val0 = int(total_mem * float(range1[0].strip()) / 100 / page_size)
            range1_val1 = int(total_mem * float(range1[1].strip()) / 100 / page_size)
            range2_val0 = int(total_mem * float(range2[0].strip()) / 100 / page_size)
            range2_val1 = int(total_mem * float(range2[1].strip()) / 100 / page_size)
            if not (range0_val0 <= curren_value0 <= range0_val1) or \
                    not (range1_val0 <= curren_value1 <= range1_val1) or \
                    not (range2_val0 <= curren_value2 <= range2_val1):
                return False, "Current:[%s],Request:[%s]" % (current_value, check_policy['range'])
        else:
            request_val = self._get_tcp_mem(check_policy)
            if current_value.replace(' ', '') != request_val.replace(' ', ''):
                return False, "Current:[%s],Request:[%s]" % (current_value, request_val)

        return True, ""

    ############################################################
    # COMMON INTERFACE : whether the parameter need to be set.
    ############################################################
    def is_need_set(self, para_key, check_policy, current_value):
        """
        :param para_key:
        :param check_policy:
        :param current_value:
        :return:
        """
        if self._get_is_upgrade_preinstall():
            setup_policy = check_policy.get("upgrade", NO_SETUP)
        else:
            setup_policy = check_policy.get("install", NO_SETUP)

        if setup_policy == NO_SETUP:
            ret = False
        elif setup_policy == FORCE_SETUP:
            ret = True
        elif setup_policy == OUT_RANGE_SETUP:
            ret = not self.check_parameter(para_key, check_policy, current_value)
        elif setup_policy == NEW_ADD_SETUP:
            ret = self.pkg_version == check_policy.get('add_ver')
        else:
            ret = False

        self.logger.debug("[%s][%s][%s][%s][%s][%s]." %
                          (para_key, check_policy, current_value,
                           self._get_is_upgrade_preinstall(), self.pkg_version, ret))

        return ret

    ############################################################
    # Check and set sysctl parameter.
    ############################################################
    def get_sysctl_list(self):
        cmd = "sysctl -a"
        (status, output) = DefaultValue.retryGetstatusoutput(cmd)
        if status != 0:
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd + " Error:\n%s" % output)

        self.logger.debug("sysctl parameters:\n%s." % output)
        result_dict = {}
        for line in output.splitlines():
            if not line:
                continue
            items = line.split("=")
            if len(items) != 2:
                continue

            result_dict[items[0].strip()] = items[1].strip()

        return result_dict

    def set_sysctl_para(self, para_key, check_policy, current_value):
        if not self.is_need_set(para_key, check_policy, current_value):
            self.logger.debug("Parameter [%s] is not modified." % para_key)
            return

        if para_key == "vm.min_free_kbytes":
            value = self._get_min_free_kbytes(check_policy)
        elif para_key == "net.ipv4.tcp_mem":
            value = self._get_tcp_mem(check_policy)
        else:
            value = check_policy['value']

        self._add_sysctl_to_conf(para_key, value)

    @staticmethod
    def _get_pkg_version():
        ver = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+').search(str(VersionInfo.COMMON_VERSION))
        if ver is not None:
            return ver.group()
        else:
            return None

    def _get_is_upgrade_preinstall(self):
        if self.is_upgrade_preinstall is None:
            if self.cluster_info:
                app_path = os.path.join(self.cluster_info.appPath, "bin/gaussdb")
                self.is_upgrade_preinstall = os.path.exists(app_path)
            else:
                self.is_upgrade_preinstall = False

        return self.is_upgrade_preinstall

    def _add_sysctl_to_conf(self, para, value):
        self.logger.debug("Adding sysctl parameter %s = %s." % (para, value))
        cmd = "sed -i '/^\\s*%s *=.*$/d' %s && echo %s = %s >> %s" % \
              (para, SECTION_SYSCTL, para, value, SECTION_SYSCTL)
        self.logger.debug("Command:%s" % cmd)
        (status, output) = DefaultValue.retryGetstatusoutput(cmd)
        if status != 0:
            self.logger.log("%sFailed to set variable '%s %s'." % (FORMAT, para, value))
            self.logger.debug("Command:%s.Output:%s." % (cmd, str(output)))
        else:
            self.logger.log("%sSet variable '%s' to '%s'" % \
                            (FORMAT, para, value))

    def delete_parameter_from_sysctl(self, para):
        """
        """
        self.logger.debug("Deleting sysctl parameter %s." % para)
        cmd = "sed -i '/^\\s*%s *=.*$/d' %s" % (para, SECTION_SYSCTL)
        self.logger.debug("Command:%s" % cmd)
        (status, output) = DefaultValue.retryGetstatusoutput(cmd)
        if status != 0:
            self.logger.debug("%sFailed to delete variable '%s' from %s." % \
                              (FORMAT, para, SECTION_SYSCTL))
            self.logger.debug("Command:%s,Output:%s." % (cmd, str(output)))

    ############################################################
    # Check and set limits parameter.
    ############################################################
    def get_limits_list(self):
        cmd = "su - %s -c 'ulimit -a'" % self.user
        (status, output) = DefaultValue.retryGetstatusoutput(cmd)
        self.logger.debug("limits parameters:\n%s." % output)
        if status != 0:
            raise Exception(ErrorCode.GAUSS_514["GAUSS_51400"] % cmd + " Error:\n%s" % output)

        result_dict = {}
        for line in output.splitlines():
            if not line:
                continue
            key = line.split('(')[0].strip()
            val = line.split()[-1].strip()

            result_dict[key] = val

        return result_dict

    def set_limits_para(self, para_key, check_policy, current_value):
        if not self.is_need_set(para_key, check_policy, current_value):
            self.logger.debug("Parameter [%s] is not modified." % para_key)
            return

        self._add_limits_to_conf(para_key, check_policy['value'])

    def _add_limits_to_conf(self, para, value):
        self.logger.debug("Setting limits config.")
        limits_files = [SECTION_LIMITS]
        if para == "open files":
            if os.path.exists("/etc/security/limits.d/91-nofile.conf"):
                limits_files.append("/etc/security/limits.d/91-nofile.conf")
            if os.path.exists("/etc/security/limits.d/90-nofile.conf"):
                limits_files.append("/etc/security/limits.d/90-nofile.conf")

        item = LIMITS_MAP[para]
        cmd = "hostname"
        for limits_file in limits_files:
            cmd += " && sed -i -e '/^.* soft *%s .*$/d' -e '/^.* hard *%s .*$/d' %s" % \
                   (item, item, limits_file)
            cmd += " && echo '* soft %s %s' >> %s && echo '* hard %s %s' >> %s" % \
                   (item, value, limits_file,
                    item, value, limits_file)
        self.logger.debug("Command:%s" % cmd)
        (status, output) = DefaultValue.retryGetstatusoutput(cmd)
        if status != 0:
            self.logger.log("%sFailed to set variable '%s %s'. Error: \n%s" % \
                            (FORMAT, para, value, output))
        else:
            self.logger.log("%sSet variable [%s(%s)] to [%s] in %s." %
                            (FORMAT, para, item, value, limits_files))
