# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
import time
import utils.common.log as logger
from utils.common.fic_base import TestCase
from utils.common.exception import HCCIException
from utils.business.project_condition_utils import get_project_condition_boolean
from plugins.DistributedStorage.logic.deploy_operate import DeployOperate
from plugins.DistributedStorage.logic.install_operate import InstallOperate
from plugins.DistributedStorage.utils.common.deploy_constant import DeployConstant
from plugins.DistributedStorage.Deploy.scripts.BusinessSeparate.implement.tc_create_pool import CreatePool
from plugins.DistributedStorage.logic.sandbox_operate import SandBoxHandlerForDeploy, ENABLE_STATUS


class CreateNewPool(TestCase):
    def __init__(self, project_id, pod_id, fs_args, **kwargs):
        super(CreateNewPool, self).__init__(project_id, pod_id)
        self.opr = DeployOperate(fs_args)
        self.install_operate = InstallOperate(project_id, pod_id, fs_args)
        self.fsa_list = fs_args.get('fsa_list')
        self.osd_list = fs_args.get('osd_nodes_info')
        self.float_ip = fs_args.get('float_ip')
        self.update_pwd = fs_args.get('dm_update_pwd')
        self.root_pwd = fs_args.get('fsm_root_pwd')
        self.fs_args = fs_args
        self.host_and_tool = (self.float_ip, None)
        self.main_storage_type = dict()
        self.main_storage_capacity = dict()
        self.cache_type_mark = dict()
        self.cache_capacity_mark = dict()
        self.min_disk_num = 0
        self.max_disk_num = 0
        self.disk_num = 0
        self.more_args = kwargs
        self.create_tool = CreatePool(self.project_id, self.pod_id, fs_args)
        self.is_create_manage_pool = get_project_condition_boolean(self.project_id, "ManageStorNewPool")

    @classmethod
    def open_disk_vendor_switch(cls, sandbox_handle, install_operate: InstallOperate):
        if ENABLE_STATUS in sandbox_handle.sandbox_initial_status.values():
            sandbox_handle.disable_fsm_sandbox()
            try:
                install_operate.config_disk_type_switch(support=True)
            finally:
                sandbox_handle.recovery_sandbox_status()
        else:
            install_operate.config_disk_type_switch(support=True)

    @classmethod
    def variable_check_and_assignment(cls, src_dict, src_key, target_dict, target_key):
        if not src_dict.get(src_key):
            err_msg = "The parameter[%s] cannot be empty in LLD" % src_key
            logger.error(err_msg)
            raise Exception(err_msg)
        src_value = src_dict.get(src_key)
        target_value = target_dict.get(target_key)
        if target_value and target_value != src_value:
            err_msg = "Inconsistent %s[%s!=%s] for the same resource pool" % (src_key, target_value, src_value)
            logger.error(err_msg)
            raise Exception(err_msg)
        return src_value

    @classmethod
    def type_check_and_assign(cls, src_value, check_value, default_value=0):
        if not check_value:
            return default_value
        if src_value and check_value != src_value:
            err_msg = "Inconsistent encrypt type. Both types of encryption and unencryption are not supported " \
                      "in the same storage pool."
            logger.error(err_msg)
            raise Exception(err_msg)
        return check_value

    @staticmethod
    def var_compare(node, src_value, cmp_data, cmp_type):
        cmp_value = cmp_data.get('dev_value')
        if src_value != cmp_value:
            cmp_node = cmp_data.get('node_ip')
            err_msg = "The %s[%s] on the current node [%s] is inconsistent with the %s[%s] on the first node [%s]." \
                      % (cmp_type, src_value, node, cmp_type, cmp_value, cmp_node)
            logger.error(err_msg)
            raise HCCIException(626105, node, cmp_type, src_value, cmp_node, cmp_type, cmp_value)
        return src_value

    @staticmethod
    def record_disk_num(min_num, max_num, disk_num):
        if min_num == 0:
            min_num = disk_num
        else:
            if disk_num < min_num:
                min_num = disk_num
        if max_num == 0:
            max_num = disk_num
        else:
            if disk_num > max_num:
                max_num = disk_num
        return min_num, max_num

    @staticmethod
    def _format_pool_error_info(pool_name, task_status, description, err_msg):
        task_data = dict()
        task_data['pool'] = pool_name
        task_data['status'] = task_status
        task_data['description'] = description
        task_data['detail'] = err_msg
        return task_data

    def procedure(self):
        if not len(self.fsa_list) >= len(self.osd_list):
            logger.error("The osd list is not the subset of fsa list!")
            raise Exception("The osd list is not the subset of fsa list!")

        logger.info("Start to create pool.")
        if self.is_create_manage_pool:
            # 管理存储：需要关闭重删压缩；还需关闭沙箱后，ssh登录FSM修改配置，支持不同厂商缓存盘共池
            sandbox_handle = SandBoxHandlerForDeploy(self.fs_args)
            logger.info("sandbox initial state of FSM:{}".format(sandbox_handle.sandbox_initial_status))
            self.open_disk_vendor_switch(sandbox_handle, self.install_operate)
            sandbox_handle.opr.disable_dedup_compress()

        self.opr.login(DeployConstant.DM_LOGIN_USER, self.update_pwd)
        try:
            logger.info('Get data of storage pool')
            pool_config_list = self.get_pool_config_params()
            logger.info('Creating pool')
            fail_pool = list()
            fail_pool_detail = list()
            self.create_pool(fail_pool, fail_pool_detail, pool_config_list)
            if len(fail_pool) > 0:
                err_msg = "Failed to create pool%s, Detail:%s" % (fail_pool, fail_pool_detail)
                logger.error(err_msg)
                raise Exception(err_msg)
            logger.info('Create pool successful')
        finally:
            self.opr.login_out(DeployConstant.DM_LOGIN_USER, self.update_pwd)

    def create_pool(self, fail_pool, fail_pool_detail, pool_config_list):
        for pool in pool_config_list:
            res = self.opr.create_pool(pool)
            status_code, result, task_id, error_code, error_des = res.get_create_pool_code()
            logger.info('Creating pool interface response status_code: %s ,result: %s' % (status_code, result))
            if status_code != 200 or result != 0:
                logger.info("Pool args is: %s" % pool)
                err_msg = "Failed to Create pool, " \
                          "Detail:[status:%s,result:%s,error:%s]%s" % (status_code, result, error_code, error_des)
                logger.error(err_msg)
                sector_msg = "The sector size of the specified disk list varies " \
                             "or is different from that of disks in the storage pool"
                if sector_msg in error_des:
                    raise HCCIException(626400, err_msg)
                raise HCCIException(626003, err_msg)
            query_result = self.query_pool_task_process(task_id, pool.get('poolPara').get('poolName'))
            if query_result:
                fail_pool.append(pool.get('poolPara').get('poolName'))
                fail_pool_detail.append(query_result)

    def get_pool_config_params(self):
        is_new_business_pool = get_project_condition_boolean(self.project_id, 'TenantStorNewPool')
        if is_new_business_pool:
            all_nodes_disks = self.get_cluster_disk_info()
            exist_pools = self.get_exist_pool_info()
            pool_config_list = self.create_tool.get_create_pool_args(self.osd_list, all_nodes_disks, exist_pools)
        else:
            # 管理存储扩新池
            pool_config_list = self.get_and_check_pool_args(self.osd_list)
        return pool_config_list

    def query_pool_task_process(self, task_id, pool_name, timeout=3600):
        logger.info('Query the process of creating pool[%s]' % pool_name)
        current_time = 0
        task_data = dict()
        while current_time <= timeout:
            time.sleep(10)
            current_time += 10
            logger.info("Start query the process of pool[%s] task[%s] after %s seconds." % (pool_name, task_id,
                                                                                            current_time))
            res_query = self.opr.query_task_info()
            task_info = res_query.get_task_by_id(task_id)
            task_status = task_info.get('taskStatus')
            entity_name = task_info.get('entityName')
            if task_status == 'success':
                logger.info("Create the storage pool[%s] success." % entity_name)
                break
            elif task_status == "failed":
                logger.error("Failed to create the storage pool[%s]. Detail:%s" % (entity_name, str(task_info)))
                task_data = self._format_pool_error_info(
                    pool_name, task_status, "Failed to create the storage pool", task_info)
                break
            elif task_status == "part_success":
                err_hint = "The task of creating a pool is partially successful. " \
                           "The OSD process fails to be started or other sub-task fail to be created on some nodes. " \
                           "Make the following operations by Manually: " \
                           "1. Login to FusionStorage web page and go to the task center, " \
                           "find the task of creating a pool, and handle the faulty disk or failed sub-task. " \
                           "2. Delete the created storage pool. " \
                           "If a message is displayed indicating that the storage pool depends on the VBS, " \
                           "enable the VBS first, delete the storage pool, " \
                           "and then disable the VBS. Try again after the pool is deleted."
                logger.error(err_hint)
                task_data = self._format_pool_error_info(pool_name, task_status, err_hint, task_info)
                break
            else:
                task_name = task_info.get('taskName')
                task_progress = task_info.get('progress')
                logger.info("Creating the storage pool[taskID: %s, taskName: %s, taskObject: %s, taskStatus: %s, "
                            "taskProgress: %s]" % (task_id, task_name, entity_name, task_status, task_progress))

        if current_time >= timeout:
            err_msg = "Waiting for the task[%s] completion times out after %s seconds." % (task_id, current_time)
            logger.error(err_msg)
            task_data = self._format_pool_error_info(
                pool_name, 'timeout', "Waiting for the task completion times out.", err_msg)
        return task_data

    def get_and_check_pool_args(self, osd_nodes):
        pool_config_args_list = list()
        all_nodes_disks = self.opr.query_all_disk()
        encrypt_type_map = {'Normal': 0, 'Encryption': 1}
        duplicate_pool_list = [
            node.get('storage_pool_name_and_slot')
            for node in osd_nodes
            if node.get('storage_pool_name_and_slot')
        ]
        unique_pool_list = self._get_and_check_pool_list(duplicate_pool_list)
        # 检查戴尔服务器场景是否安装了扫盘工具
        self.install_operate.check_third_party_server_disk(all_nodes_disks, osd_nodes)

        for pool in unique_pool_list:
            if duplicate_pool_list.count(pool) < 3:
                err_msg = "The number of nodes in the storage pool[%s] is less than 3 in LLD" % pool
                logger.error(err_msg)
                raise Exception(err_msg)
            self.main_storage_type = dict()
            self.main_storage_capacity = dict()
            self.cache_type_mark = dict()
            self.cache_capacity_mark = dict()
            self.min_disk_num = 0
            self.max_disk_num = 0
            self.disk_num = 0
            pool_para_args = dict()
            pool_config_args = dict()
            pool_para_args['poolName'] = pool
            server_list = list()
            for osd in osd_nodes:
                if pool != osd.get("storage_pool_name_and_slot"):
                    continue
                self._get_pool_redundancy_policy(osd, pool_para_args, encrypt_type_map)
                server = self._get_pool_server_info(osd, pool_para_args, all_nodes_disks)
                server_list.append(server)

            self.check_disk_num()
            pool_para_args['storageMediaType'] = self.main_storage_type.get('dev_value')
            if get_project_condition_boolean(self.project_id, 'TenantStorFB_RackHA'):
                pool_para_args['securityLevel'] = 'rack'
            else:
                pool_para_args['securityLevel'] = 'server'
            pool_config_args['poolPara'] = pool_para_args
            pool_config_args['serverList'] = server_list
            pool_config_args_list.append(pool_config_args)
            logger.info("Get pool parameters:%s" % str(pool_config_args))
        return pool_config_args_list

    def get_main_storage_disk_list(self, osd, all_nodes_disks, start_slot, end_slot):
        storage_disks = list()
        om_ip = osd.get('om_ip')
        for slot in range(int(start_slot), int(end_slot)+1):
            disk_meta = all_nodes_disks.get_disks_by_node_slot(om_ip, slot)
            if not disk_meta:
                logger.info("There is no disk on slot[%s] at node[%s]" % (slot, om_ip))
                continue
            disk_status = disk_meta.get('devRole')
            if disk_status != 'no_use':
                logger.info("The status[%s] of slot[%s] is not 'no_use'on node[%s]" % (disk_status, slot, om_ip))
                raise HCCIException(626112, om_ip, slot, disk_status)
            disk = dict()
            disk['phySlotId'] = slot
            disk['mediaRole'] = "main_storage"
            if not self.main_storage_type:
                self.main_storage_type['node_ip'] = om_ip
                self.main_storage_type['dev_value'] = disk_meta.get('devType')
                disk['mediaType'] = self.main_storage_type.get('dev_value')
            else:
                disk['mediaType'] = self.var_compare(om_ip, disk_meta.get('devType'), self.main_storage_type,
                                                     "Disk Type")
            if not self.main_storage_capacity:
                self.main_storage_capacity['node_ip'] = om_ip
                self.main_storage_capacity['dev_value'] = disk_meta.get('devTotalCapacity')
                disk['mediaSize'] = self.main_storage_capacity.get('dev_value')
            else:
                disk['mediaSize'] = self.var_compare(om_ip, disk_meta.get('devTotalCapacity'),
                                                     self.main_storage_capacity, "Disk Capacity")
            disk['phyDevEsn'] = disk_meta.get('devEsn')
            storage_disks.append(disk)

        disk_num = len(storage_disks)
        if disk_num < 4:
            err_msg = "main storage disk cannot be less than 4[current no use disk:%s]" % disk_num
            logger.error(err_msg)
            slot_range = '%s-%s' % (start_slot, end_slot)
            raise HCCIException(626103, om_ip, slot_range, disk_num)
        self.disk_num += disk_num
        self.min_disk_num, self.max_disk_num = self.record_disk_num(self.min_disk_num, self.max_disk_num, disk_num)
        return storage_disks

    def get_cache_disk_list(self, osd, all_nodes_disks, cache_type, main_storage_type):
        cache_disks = list()
        om_ip = osd.get('om_ip')
        if cache_type and main_storage_type not in ['ssd_disk', 'ssd_card']:
            all_disks = all_nodes_disks.get_disks_by_node(om_ip)
            cache_num = 0
            max_cache_num = 4
            for cache in all_disks:
                cache_num += self._get_cache_info_for_node(cache, cache_type, om_ip, cache_disks)
                if cache_num >= max_cache_num:
                    break

            if len(cache_disks) == 0:
                err_msg = "There is no or no unused cache disk[%s] on node[%s]." % (cache_type, om_ip)
                logger.error(err_msg)
                raise HCCIException(626104, om_ip, cache_type)
        return cache_disks

    def check_disk_num(self):
        if self.max_disk_num == 0 or self.min_disk_num == 0:
            logger.info("No available disk num[max:%s, min:%s]. Skip.." % (self.max_disk_num, self.min_disk_num))
            return

        if self.max_disk_num - self.min_disk_num > 2:
            err_msg = "The difference between maximal[%s] and minimal[%s] main storage number is more than 2." \
                      % (self.max_disk_num, self.min_disk_num)
            logger.error(err_msg)
            raise Exception(err_msg)
        try:
            percentage = (self.max_disk_num - self.min_disk_num) / float(self.max_disk_num)
        except ZeroDivisionError as e:
            err_msg = 'ZeroDivisionError, Detail:%s' % str(e)
            logger.error(err_msg)
            raise e
        if percentage > 0.3:
            percentage *= 100
            err_msg = "The difference[%s%%] between maximal and minimal main storage number is more than 30%% of " \
                      "maximax number." % percentage
            logger.error(err_msg)
            raise Exception(err_msg)
        if self.disk_num < 12:
            err_msg = "The total main storage number is less than 12."
            logger.error(err_msg)
            raise Exception(err_msg)

    def get_cluster_disk_info(self):
        logger.info('Exp new pool:Query all disk info')
        all_nodes_disks = self.opr.query_all_disk()
        all_disk_data = all_nodes_disks.get_query_data()
        disk_info = all_disk_data.get('disks')
        if not disk_info:
            err_msg = 'Exp new pool: No disk information exists.'
            logger.error(err_msg)
            raise HCCIException(626074, all_disk_data)
        return all_nodes_disks

    def get_exist_pool_info(self):
        logger.info("[Exp new pool]Query storage pool data")
        exist_pool = list()
        res_pool = self.opr.query_storage_pool()
        pool_info = res_pool.get_query_data()
        logger.info('[Exp new pool]Pool info:{}'.format(pool_info))
        storage_pools = pool_info.get('storagePools')
        if storage_pools:
            exist_pool = [pool.get('poolName') for pool in storage_pools]
            logger.info("[Exp new pool]Pool[%s] has been created." % exist_pool)
        return exist_pool

    def _get_and_check_pool_list(self, duplicate_pool_list):
        """
        获取并检查存储池信息
        :param duplicate_pool_list:1.4重复的池列表
        :return:
        """
        unique_pool_list = list(set(duplicate_pool_list))
        if not unique_pool_list:
            err_msg = "The storage pool name in the LLD table cannot be empty."
            logger.error(err_msg)
            raise Exception(err_msg)
        logger.info("Query storage pool data first.")
        res_pool = self.opr.query_storage_pool()
        pool_info = res_pool.get_query_data()
        storage_pools = pool_info.get('storagePools')
        if storage_pools:
            for pool in storage_pools:
                pool_name = pool.get('poolName')
                if pool_name in unique_pool_list:
                    err_msg = "Pool[%s] has been created." % pool_name
                    logger.error(err_msg)
                    raise Exception(err_msg)
        return unique_pool_list

    def _get_pool_server_info(self, osd, pool_para_args, all_nodes_disks):
        """
        获取创建存储池节点信息
        :param osd: 节点信息
        :param pool_para_args: 创池参数
        :param all_nodes_disks: 所有节点盘信息
        :return:
        """
        server = dict()
        server['nodeMgrIp'] = osd.get("om_ip")
        osd_slot = osd.get('primary_slot')
        if not osd_slot:
            err_msg = "The main storage slot cannot be empty in LLD for osd node."
            logger.error(err_msg)
            raise Exception(err_msg)
        start_slot = osd_slot.split('-')[0]
        end_slot = osd_slot.split('-')[1]
        storage_disk_list = self.get_main_storage_disk_list(osd, all_nodes_disks, start_slot, end_slot)
        disk_type = self.main_storage_type.get('dev_value')
        if disk_type in ['sas_disk', 'sata_disk']:
            server['startSlot'] = start_slot
            server['endSlot'] = end_slot
            cache_type = osd.get('cache_type')
        # 如果主存类型为ssd_disk/ssd_card, 则池为全闪，缓存为none
        elif disk_type in ['ssd_disk']:
            server['startSlot'] = start_slot
            server['endSlot'] = end_slot
            cache_type = 'none'
        else:
            cache_type = 'none'
        server['cacheMediaType'] = cache_type
        pool_para_args['cacheMediaType'] = cache_type
        cache_disk_list = self.get_cache_disk_list(osd, all_nodes_disks, cache_type, disk_type)
        server['mediaList'] = storage_disk_list + cache_disk_list
        return server

    def _get_pool_redundancy_policy(self, osd, pool_para_args, encrypt_type_map):
        """
        获取检查存储池容斋策略是否一致
        :param osd: 节点信息
        :param pool_para_args: 创池参数
        :param encrypt_type_map: 加密方式
        :return:
        """
        redundancy_policy = osd.get('storagepool_redundancy_policy')
        if redundancy_policy:
            if redundancy_policy == 'ec':
                pool_para_args['redundancyPolicy'] = redundancy_policy
                pool_para_args['numParityUnits'] = self.variable_check_and_assignment(osd,
                                                                                      'ec_verify_fragments',
                                                                                      pool_para_args,
                                                                                      'numParityUnits')
                pool_para_args['numDataUnits'] = self.variable_check_and_assignment(osd, 'ec_data_fragments',
                                                                                    pool_para_args,
                                                                                    'numDataUnits')
                pool_para_args['numFaultTolerance'] = 1
            elif redundancy_policy == '3Redundancy':
                pool_para_args['redundancyPolicy'] = 'replication'
                pool_para_args['replicaNum'] = 3
            else:
                err_msg = "The redundancy policy[%s] is not ec and 3redundancy" % redundancy_policy
                logger.error(err_msg)
                raise Exception(err_msg)
            encrypt_type = osd.get('storage_pool_type')
            encrypt_type = encrypt_type_map.get(encrypt_type)
            pool_para_args['encryptType'] = self.type_check_and_assign(pool_para_args.get('encryptType'),
                                                                       encrypt_type, 0)
        else:
            pool_para_args['redundancyPolicy'] = 'replication'
            pool_para_args['replicaNum'] = 3
            pool_para_args['encryptType'] = 0

    def _get_cache_info_for_node(self, cache, cache_type, om_ip, cache_disks):
        """
        获取和校验单个节点缓存容量和类型
        :param cache: 缓存信息
        :param cache_type: lld指定缓存类型
        :param om_ip: 节点om ip
        :param cache_disks: 创建存储池缓存信息
        :return:
        """
        if cache.get('devType') == cache_type\
                and cache.get('devRole') == 'no_use'\
                and cache.get('devSlot') < 5000:
            cache_disk = dict()
            cache_disk['phySlotId'] = cache.get('devSlot')
            cache_disk['mediaRole'] = "osd_cache"
            if not self.cache_type_mark:
                self.cache_type_mark['node_ip'] = om_ip
                self.cache_type_mark['dev_value'] = cache.get('devType')
                cache_disk['mediaType'] = self.cache_type_mark.get('dev_value')
            else:
                cache_disk['mediaType'] = self.var_compare(om_ip, cache.get('devType'), self.cache_type_mark,
                                                           "Cache Type")
            if not self.cache_capacity_mark:
                self.cache_capacity_mark['node_ip'] = om_ip
                self.cache_capacity_mark['dev_value'] = cache.get('devTotalCapacity')
                cache_disk['mediaSize'] = self.cache_capacity_mark.get('dev_value')
            else:
                cache_disk['mediaSize'] = self.var_compare(om_ip, cache.get('devTotalCapacity'),
                                                           self.cache_capacity_mark, "Cache Capacity")
            cache_disk['phyDevEsn'] = cache.get('devEsn')
            cache_disks.append(cache_disk)
            return 1
        return 0
