# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
import time
import utils.common.log as logger
import utils.constant.constant as constant
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.install_operate import InstallOperate
from plugins.DistributedStorage.utils.common.deploy_constant import DeployConstant
from plugins.DistributedStorage.logic.deploy_operate import DeployOperate


class CreatePool(TestCase):
    def __init__(self, project_id, pod_id, fs_args, **kwargs):
        super(CreatePool, self).__init__(project_id, pod_id)
        self.fs_args = fs_args
        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_list')
        self.float_ip = fs_args.get('float_ip')
        self.update_pwd = fs_args.get('dm_update_pwd')
        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.config_license_switch_condition = get_project_condition_boolean(self.project_id, '!TenantStorFBReuse80')

    @staticmethod
    def variable_check_and_assignment(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 1.1Sheet" % 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

    @staticmethod
    def type_check_and_assign(src_value, check_value, default_value=0):
        if not src_value:
            return default_value
        if not check_value:
            return default_value
        if 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 src_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

    @staticmethod
    def _get_and_check_slot_range(osd, pool):
        """
        获取主存槽位
        :param osd:
        :param pool:
        :return:
        """
        osd_slot = osd.get('primary_slot')
        if not osd_slot:
            err_msg = "The main storage slot cannot be empty in LLD 1.1sheet for pool[%s]" % pool
            logger.error(err_msg)
            raise Exception(err_msg)
        start_slot = osd_slot.split('-')[0]
        end_slot = osd_slot.split('-')[1]
        return start_slot, end_slot

    def procedure(self):
        try:
            logger.info("Start to create pool.")
            if self.config_license_switch_condition:
                self.install_operate.config_create_pool_license_switch(
                    self.pod_id, self.float_ip)

            self.login_dm()

            self._check_fsa_and_osd_list()

            self._check_cluster()

            logger.info('Get data of storage pool')
            pool_config_list = self.get_and_check_pool_args(self.osd_list)

            logger.info('Creating pool')
            fail_pool = 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 HCCIException(626003, err_msg)
            logger.info('Create pool successful')
        finally:
            if self.config_license_switch_condition:
                self.install_operate.config_create_pool_license_switch(
                    self.pod_id, self.float_ip, delete=True)
            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()
            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('pool_name'))
            if query_result:
                fail_pool.append(pool.get('pool_name'))
                fail_pool_detail.append(query_result)

    def login_dm(self):
        """
        登陆存储
        :return:
        """
        status_code, error_code, error_des = self.opr.login(DeployConstant.DM_LOGIN_USER, self.update_pwd)
        if status_code != 200 or error_code != 0:
            err_msg = "Failed to login deploy manager, Detail:[status:%s,code:%s]%s" % (status_code, error_code,
                                                                                        error_des)
            logger.error(err_msg)
            raise Exception(err_msg)

    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):
        all_openstack_nodes = self.db.get_ref_component_host_list(self.pod_id, constant.REFCOMPONENTCONSTANT.OSD)
        osd_list = list()
        for node in all_openstack_nodes:
            node_dict = node.__dict__
            for osd in osd_nodes:
                if node_dict.get('bmc_ip') == osd.get('bmc_ip'):
                    osd_node = dict(list(node_dict.items())+list(osd.items()))
                    osd_list.append(osd_node)
        pool_config_args_list = list()
        all_nodes_disks = self.opr.query_all_disk()
        encrypt_type_map = {'Normal': 0, 'Encryption': 1}
        pool_name = self.db.get_user_input_cloud_param_by_key(self.project_id, "business_storage_pool")
        pool_list = [pool_name]
        for pool in pool_list:
            pool_para_args = dict()
            pool_config_args = dict()
            pool_para_args['poolName'] = pool
            server_list = list()
            for osd in osd_list:
                self._get_and_check_redundancy_policy(osd, pool_para_args)
                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)
                server = dict()
                server['nodeMgrIp'] = osd.get("om_ip")
                start_slot, end_slot = self._get_and_check_slot_range(osd, pool)
                storage_disk_list = self.get_main_storage_disk_list(osd, all_nodes_disks, start_slot, end_slot)
                cache_type, disk_type = self._check_cache_type(server, osd, start_slot, end_slot)
                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
                server_list.append(server)

            self.check_disk_num()
            pool_para_args['storageMediaType'] = self.main_storage_type.get('dev_value')
            self._get_security_level(pool_para_args, pool)
            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:
                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"
                    self._check_cache_type_and_capacity(om_ip, cache, cache_disk)
                    cache_disk['phyDevEsn'] = cache.get('devEsn')
                    cache_num += 1
                    cache_disks.append(cache_disk)
                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 - 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_and_check_redundancy_policy(self, osd, pool_para_args):
        """
        检查存储池容灾策略
        :param osd:
        :param pool_para_args:
        :return:
        """
        redundancy_policy = osd.get('storagepool_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)

    def _check_cache_type(self, server, osd, start_slot, end_slot):
        """
        根据主存类型判断缓存是否为none
        :param server: 创池节点参数
        :param osd: 节点参数
        :param start_slot: 主存槽位开始
        :param end_slot:  主存槽位结束
        :return:
        """
        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'
        return cache_type, disk_type

    def _get_security_level(self, pool_para_args, pool):
        if get_project_condition_boolean(self.project_id, 'TenantStorFB_RackHA'):
            if pool_para_args.get('redundancyPolicy') == 'ec':
                err_msg = "The redundancy policy is EC only when the server level is supported " \
                          "in LLD 1.1Sheet" % pool
                logger.error(err_msg)
                raise Exception(err_msg)
            pool_para_args['securityLevel'] = 'rack'
        else:
            pool_para_args['securityLevel'] = 'server'

    def _check_cache_type_and_capacity(self, om_ip, cache, cache_disk):
        """
        检查创池节点缓存类型和容量是否一致
        :param om_ip:
        :param cache:
        :param cache_disk:
        :return:
        """
        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")

    def _check_fsa_and_osd_list(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!")

    def _check_cluster(self):
        logger.info('Check cluster...')
        response = self.opr.query_manage_cluster()
        cluster = response.get_query_data()
        if not cluster.get('clusterName'):
            logger.error('Query cluster fail...')
            raise Exception("Query cluster fail...")
        else:
            logger.info('Query cluster successfully...')
