#!/usr/bin/env python
# -*- 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.exception import FCDException
from utils.common.fic_base import TestCase
from platforms.project.ProjectUtils import get_project_condition_boolean
from plugins.DistributedStorage.scripts.utils.common.DeployConstant import DeployConstant
from plugins.DistributedStorageReplication.scripts.common_utils.config_params import Params
from plugins.DistributedStorageReplication.scripts.common_utils.rest_operate import RestOperate


class CreateRepControlCluster(TestCase):
    def __init__(self, project_id, pod_id):
        super(CreateRepControlCluster, self).__init__(project_id, pod_id)
        self.project_id = self.args_dict['project_id']
        self.pod_id = self.args_dict['pod_id']
        self.service_name = "FusionStorageBlockReplication"
        self.opr = None

    @staticmethod
    def get_shared_node_disk_by_role(zk_node_disk_infos):
        """
        复制元数据盘、基础元数据盘融合，选择role为zk_disk的盘
        介质类型是ssd_card时，换算率是1000
        disk_infos example:
            {"devType": "ssd_card",
             "devEsn": "***",
             "devStatus": 0,
             "devRole": "no_use",
             "devTotalCapacity": 11444224,
             "logicSectorSize": 512,
             "devName": "nvme0n1",
             "devMode": 1,
             "devTotalCapacityForByte": 12000138625024,
             "vendor": 4,
             "name": "4:nvme0n1",
             "supportEncrypt": 0,
             "running_days": "--",
             "devUsedCapacity": 1596000,
             "sectorSize": 512,
             "id": "*:***",
             "devSlot": 44,
             "wear_degree": "--"}
        :param zk_node_disk_infos:
        :return:
        """
        logger.info('Start grouping the disks')
        different_disk_type = dict()
        node_zk_disk = dict()
        under_capacity = dict()
        for ip, disk_infos in zk_node_disk_infos.items():
            for disk_info in disk_infos:
                if disk_info.get('devRole') != 'zk_disk':
                    continue
                if disk_info.get('devTotalCapacity') // 1000 < 105:
                    under_capacity[ip] = disk_info
                node_zk_disk[ip] = disk_info
                if disk_info.get('devType') != 'ssd_card':
                    different_disk_type[ip] = disk_info.get('devType')
                break
        if different_disk_type:
            logger.error('These nodes do not have available nvme disks:{}'.format(different_disk_type))
            raise FCDException(627450, list(different_disk_type.keys()), different_disk_type)
        if under_capacity:
            logger.error('The capacity of the metadata disk is insufficient:{}'.format(under_capacity))
            raise FCDException(627451, list(under_capacity.keys()), under_capacity)
        return node_zk_disk

    @staticmethod
    def get_sep_node_disk_team_by_capacity(zk_node_disk_infos):
        """
        将所有zk节点的ssd_card磁盘按容量过滤后，按容量排序，取容量最小的一块盘。容量需大于105G
        介质类型是ssd_card时，换算率是1000
        disk_infos example:
            {"devType": "ssd_card",
             "devEsn": "****",
             "devStatus": 0,
             "devRole": "no_use",
             "devTotalCapacity": 11444224,
             "logicSectorSize": 512,
             "devName": "nvme0n1",
             "devMode": 1,
             "devTotalCapacityForByte": 12000138625024,
             "vendor": 4,
             "name": "4:nvme0n1",
             "supportEncrypt": 0,
             "running_days": "--",
             "devUsedCapacity": 1596000,
             "sectorSize": 512,
             "id": "*:***",
             "devSlot": 44,
             "wear_degree": "--"}
        :param zk_node_disk_infos:
        :return:
        """
        logger.info('Start to select the disks')
        available_disk_infos = dict()
        not_exist_nvme_node = dict()
        for ip, disk_infos in zk_node_disk_infos.items():
            not_exist_nvme_flag = True
            available_disk_infos[ip] = list()
            for disk_info in disk_infos:
                cur_capacity = disk_info.get('devTotalCapacity')
                cur_role = disk_info.get('devRole')
                cur_type = disk_info.get('devType')
                if cur_role != 'no_use' or cur_type != 'ssd_card' or cur_capacity // 1000 < 105:
                    continue
                not_exist_nvme_flag = False
                try:
                    available_disk_infos[ip].append(disk_info)
                except KeyError as e:
                    logger.error('Key not exist. Detail:%s' % str(e))
                    raise e
            if not_exist_nvme_flag:
                not_exist_nvme_node[ip] = disk_infos
        if not_exist_nvme_node:
            logger.error('The following nodes do not have available nvme disks:{}'.format(not_exist_nvme_node))
            raise FCDException(627450, list(not_exist_nvme_node.keys()), not_exist_nvme_node)
        for _, disk in available_disk_infos.items():
            disk.sort(key=lambda x: x.get('devTotalCapacity'))
        logger.info('Available nvme disk information sorted by quantity:{}'.format(available_disk_infos))
        select_zk_disk = dict((node_ip, disk_lst[0]) for node_ip, disk_lst in available_disk_infos.items())
        logger.info('Selected nvme disk info: {}'.format(select_zk_disk))
        return select_zk_disk

    @staticmethod
    def get_cur_slot_disk_info(cur_node_disk_info, rep_slot, rep_zk_type, under_capacity, node_ip):
        cur_slot_disk_info = None
        for disk_info in cur_node_disk_info:
            dev_type = disk_info.get('devType')
            dev_role = disk_info.get('devRole')
            dev_slot = disk_info.get('devSlot')
            if dev_slot == int(rep_slot) and dev_type == rep_zk_type and dev_role in ['no_use', 'zk_disk']:
                cur_slot_disk_info = disk_info
                if disk_info.get('devTotalCapacity') // 1024 < 105:  # 复制元数据盘需大于105G
                    under_capacity[node_ip] = disk_info
                break
        return cur_slot_disk_info

    def procedure(self):
        """
        ### 还需增加复制参数获取中的，元数据盘类型信息
        :return:
        """
        log_module_name = self.args_dict['log_module_name']
        logger.init(log_module_name)
        logger.info("[FSBR] Start to create control cluster.")
        logger.info("[FSBR] Start to get LLD params.")
        all_params = Params(self.project_id, self.pod_id, self.service_name).get_params_dict()
        cmd_passwd = all_params.get('local_admin_password')
        float_ip = all_params.get("local_storage_fsm_ip")
        rep_node_infos = all_params.get("replication_cluster_nodes_info")
        if not rep_node_infos:
            err_msg = "control cluster info is empty."
            logger.error(err_msg)
            raise FCDException(627452)
        rep_mg_ip = []
        for info in rep_node_infos:
            if info.get('is_replication_controller_cluster') == '1':
                rep_mg_ip.append(info.get('replication_manage_ip'))
        ip_str = ','.join(rep_mg_ip)
        logger.info("[FSBR] End to get LLD params")

        self.opr = RestOperate(float_ip)
        self.login_deploy_manager(cmd_passwd)

        logger.info("[FSBR] Start to check vbs, replication server and driver")
        self.evn_pre_check(ip_str)
        logger.info("[FSBR] Check vbs replication server, driver success")

        logger.info('get cluster medias params')
        media_params = self.get_rep_cluster_media_params(rep_node_infos)

        logger.info("[FSBR] Create fsm client.")
        self.create_fsm_client(media_params, cmd_passwd)

    def login_deploy_manager(self, cmd_passwd):
        status_code, error_code, error_des = self.opr.login(DeployConstant.DM_LOGIN_USER, cmd_passwd)
        if status_code != 200 or error_code != 0:
            err_msg = "Failed to login fusionstorage manager, Detail:[status:%s,code:%s]%s" % (
                status_code, error_code, error_des)
            logger.error(err_msg)
            raise FCDException(626067, err_msg)

    def create_fsm_client(self, media_params, cmd_passwd):
        result = self.opr.create_control_cluster(media_params)
        create_result = result.query_dr_cmd_result()
        if 0 == create_result:
            logger.info("[FSBR] Create control cluster success.")
            self.opr.login_out(DeployConstant.DM_LOGIN_USER, cmd_passwd)
            return

        err_msg = "[FSBR] Create control cluster fail, retry after 60s.detail: \n%s" % result.res.json()
        logger.error(err_msg)
        time.sleep(60)
        result = self.opr.create_control_cluster(media_params)
        create_result = result.query_dr_cmd_result()
        if 0 == create_result:
            logger.info("[FSBR] Create control cluster success.")
            self.opr.login_out(DeployConstant.DM_LOGIN_USER, cmd_passwd)
        else:
            err_msg = "[FSBR] Create control cluster fail, detail: \n%s" % result.res.json()
            logger.error(err_msg)
            raise FCDException(627136, err_msg)

    def evn_pre_check(self, ip_str):
        res = self.opr.query_dsware_client()
        dsware_client_list = res.get_dsware_client_list()
        ip_list = ip_str.split(",")
        logger.info('replication manage ip:{}'.format(ip_list))
        box = []
        for ip_addr in ip_list:
            if ip_addr not in dsware_client_list:
                box.append(ip_addr)
        if box:
            err_msg = "[FSBR] Check vbs result:fail.detail: %s" % dsware_client_list
            logger.error(err_msg)
            raise Exception(err_msg)

    def get_rep_cluster_media_params(self, rep_node_infos):
        rep_zk_type, zk_ip_slot, shared_disk_flag = self.get_lld_rep_zk_info(rep_node_infos)
        logger.info('zk disk type:{}, zk slot info:{}'.format(rep_zk_type, zk_ip_slot))
        zk_node_disk_infos = self.get_zk_node_disk_info_by_rest(zk_ip_slot)
        logger.info('All disk info of zk nodes:{}'.format(zk_node_disk_infos))
        rep_zk_type = DeployConstant.STORAGE_TYPE_MAP.get(rep_zk_type)
        if rep_zk_type == 'ssd_card':
            logger.info('Start to parse disk info and obtain the device name.')
            zk_disk_info = self.get_rep_cluster_zk_disk_info_when_nvme_card(zk_node_disk_infos, shared_disk_flag)
        else:
            logger.info('Start to parse disk info by slot and obtain device name.')
            zk_disk_info = self.get_rep_cluster_zk_disk_info_by_slot(zk_node_disk_infos, zk_ip_slot, rep_zk_type)
        logger.info('Disk info of Replication cluster:{}'.format(zk_disk_info))

        logger.info('Start to config the media params of the replication control')
        node_ips, medias = [], []
        for node_ip, disk_info in zk_disk_info.items():
            media_info = "manageIp:" + node_ip + "::mediaKey:ccdb_volume,deviceName:/dev/" + disk_info.get('devName')
            node_ips.append(node_ip)
            medias.append(media_info)
        logger.info('media params of the replication control cluster:{}, {}'.format(node_ips, medias))
        return ",".join(node_ips), ";".join(medias)

    def get_lld_rep_zk_info(self, rep_node_infos):
        """
        复制存储融合场景，判断复制元数据节点与基础元数据在同一个节点上的同一块盘。该场景，直接从盘信息中选取角色为zk_disk的盘
        :param rep_node_infos:
        :return:
        """
        rep_zk_types = set()
        zk_ip_slot = dict()
        shared_disk_flag = False
        for node_info in rep_node_infos:
            if node_info.get('is_replication_controller_cluster') == "1":
                zk_ip_slot[node_info.get('replication_manage_ip')] = node_info.get('replication_cluster_meta_info')
                rep_zk_types.add(node_info.get('replication_cluster_meta_type'))
        if len(rep_zk_types) != 1:
            logger.error('The disk type of replication ZK is incorrect in the LLD, details: %s.' % rep_zk_types)
            raise FCDException(627446, DeployConstant.REP_METADATA_SUPPORT_DISK_TYPE)
        rep_zk_type = rep_zk_types.pop()
        if rep_zk_type not in DeployConstant.REP_METADATA_SUPPORT_DISK_TYPE:
            logger.error('The disk type of replication ZK is incorrect in the LLD, details: %s.' % rep_zk_types)
            raise FCDException(627446, DeployConstant.REP_METADATA_SUPPORT_DISK_TYPE)
        if len(zk_ip_slot) not in [3, 5, 7]:
            logger.error('the number of metadata nodes is required to be 3 or 5 or 7. '
                         'Current quantity:{}'.format(len(zk_ip_slot)))
            raise FCDException(627106)
        if get_project_condition_boolean(
                self.project_id,
                '(CSHAStorage_TFB|CSDRStorage_TFB)&!DRStorage_TFB_Sep&!ExpansionAdCloudService'
                '&(!TenantStorFBHCI|!TenantStorFBHCI80)'):
            shared_disk_flag = self.check_rep_and_basic_disk_shared()
        return rep_zk_type, zk_ip_slot, shared_disk_flag

    def check_rep_and_basic_disk_shared(self):
        rep_nodes = self.db.get_install_os_list_info(self.pod_id, "rep")
        for node_info in rep_nodes:
            rep_zk_slot = node_info.get('replication_cluster_meta_info')
            if node_info.get('is_metadata_node') == '1' and rep_zk_slot == node_info.get('zk_slot'):
                return True
        return False

    def get_zk_node_disk_info_by_rest(self, zk_ip_slot):
        all_nodes_disks = self.opr.query_all_disk()
        logger.info('Disk information query result:{}'.format(all_nodes_disks))
        try:
            all_disk_infos = all_nodes_disks.get_query_data().get('disks')
        except AttributeError as attr_error:
            logger.error('The disk information is empty, details:{}'.format(attr_error))
            raise FCDException(627136, attr_error) from attr_error
        zk_node_disk_infos = dict((ip, info) for ip, info in all_disk_infos.items() if ip in zk_ip_slot.keys())
        if len(zk_node_disk_infos) != len(zk_ip_slot):
            not_exist_node = set(zk_ip_slot.keys()).difference(set(zk_node_disk_infos.keys()))
            err_msg = 'The node {} disk information does not exist.'.format(not_exist_node)
            logger.error(err_msg)
            raise FCDException(627136, err_msg)
        return zk_node_disk_infos

    def get_rep_cluster_zk_disk_info_when_nvme_card(self, zk_node_disk_infos, shared_disk_flag):
        """
        nvme无法根据槽位获取磁盘设备名称，取盘原则：
            1、基础业务、复制业务元数据盘合步时：直接选取角色为zk_disk的盘，按产品文档要求检查一下容量：105G
            2、基础业务、复制业务元数据盘分离时：过滤出未使用且满足105G容量的nvme盘，然后按容量升序排列，取每个节点容量最小的一块盘。
        :param zk_node_disk_infos: dict
        :param shared_disk_flag: bool
        :return: dict
        """
        logger.info('Start to get the zk disk')
        logger.info('disk flag:{}'.format(shared_disk_flag))
        if shared_disk_flag:
            # 复制元数据盘、基础元数据盘融合时，直接选择role为zk_disk的盘
            logger.info('Start to get the shared disk')
            select_zk_disk = self.get_shared_node_disk_by_role(zk_node_disk_infos)
            logger.info('Query result:{}'.format(select_zk_disk))
        else:
            select_zk_disk = self.get_sep_node_disk_team_by_capacity(zk_node_disk_infos)
            logger.info('Query result in separate deployment:{}'.format(select_zk_disk))
        return select_zk_disk

    def get_rep_cluster_zk_disk_info_by_slot(self, all_disk_data, zk_ip_slot, rep_zk_type):
        """
        根据槽位号获取节点元数据磁盘信息，安装产品文档要求，检查磁盘容量：不小于105G
        介质类型是sas_disk, sata_disk, ssd_disk时，换算率是1024

        :param all_disk_data: dict
        :param zk_ip_slot: dict
        :param rep_zk_type: dict
        :return: dict
        """
        res_zk_disk = dict()
        not_exist_disk_node = dict()
        under_capacity = dict()
        for node_ip, rep_slot in zk_ip_slot.items():
            cur_node_disk_info = all_disk_data.get(node_ip)
            logger.info("All disk on node[%s]: %s" % (node_ip, cur_node_disk_info))
            if not cur_node_disk_info:
                logger.error('Failed to obtain the disks of node:{}'.format(node_ip))
                not_exist_disk_node[node_ip] = cur_node_disk_info
                continue
            cur_slot_disk_info = self.get_cur_slot_disk_info(cur_node_disk_info, rep_slot, rep_zk_type, under_capacity,
                                                             node_ip)
            if cur_slot_disk_info is None:
                logger.error('No disk available for this node:{}, disk info:{}'.format(node_ip, cur_node_disk_info))
                not_exist_disk_node[node_ip] = cur_node_disk_info
                continue
            res_zk_disk[node_ip] = cur_slot_disk_info
        if not_exist_disk_node:
            logger.error('The following nodes do not have available {}:{}'.format(rep_zk_type, not_exist_disk_node))
            raise FCDException(627453, list(not_exist_disk_node.keys()), rep_zk_type, not_exist_disk_node)
        if under_capacity:
            logger.error('The capacity of the metadata disk is insufficient:{}'.format(under_capacity))
            raise FCDException(627451, list(under_capacity.keys()), under_capacity)
        logger.info('Disk info of Replication cluster:{}'.format(res_zk_disk))
        return res_zk_disk
