]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Huawei: Balanced FC port selection when zoning
authorWilson Liu <liuxinguo@huawei.com>
Thu, 24 Dec 2015 04:57:56 +0000 (12:57 +0800)
committerWilson Liu <liuxinguo@huawei.com>
Fri, 29 Jan 2016 08:25:50 +0000 (16:25 +0800)
Currently we always select the first two FC ports
from every controller when zoning, the rest FC ports
of the controller will never be selected. This patch
will calculate each port's load and select the first
two smallest load ports.

Implements: blueprint huawei-balanced-fc-port-selection
Change-Id: Ie9045e0d12b7cdb920b3886545772b60727cc64e

cinder/tests/unit/test_huawei_drivers.py
cinder/volume/drivers/huawei/constants.py
cinder/volume/drivers/huawei/fc_zone_helper.py
cinder/volume/drivers/huawei/huawei_driver.py
cinder/volume/drivers/huawei/rest_client.py
releasenotes/notes/balanced-fc-port-selection-fbf6b841fea99156.yaml [new file with mode: 0644]

index 2f748565af89e66f7fe81686c718e2e02b516ffe..497b7c8df8d51d62b3f21f5ea3c6e24091361830 100644 (file)
@@ -1008,7 +1008,16 @@ FAKE_GET_FC_PORT_RESPONSE = """
     "data":[{
         "RUNNINGSTATUS":"10",
         "WWN":"2000643e8c4c5f66",
-        "PARENTID":"0A.1"
+        "PARENTID":"0A.1",
+        "ID": "1114368",
+        "RUNSPEED": "16000"
+    },
+    {
+        "RUNNINGSTATUS":"10",
+        "WWN":"2009643e8c4c5f67",
+        "PARENTID":"0A.1",
+        "ID": "1114369",
+        "RUNSPEED": "16000"
     }]
 }
 """
@@ -1391,6 +1400,190 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroPair?range=[0-100]/GET'] = (
 MAP_COMMAND_TO_FAKE_RESPONSE['/splitmirror?range=[0-100]/GET'] = (
     FAKE_COMMON_SUCCESS_RESPONSE)
 
+FACK_GET_PORTG_BY_VIEW = """
+{
+    "data": [{
+        "DESCRIPTION": "Please do NOT modify this. Engine ID: 0",
+        "ID": "0",
+        "NAME": "OpenStack_PortGroup_1",
+        "TYPE": 257
+    }],
+    "error": {
+        "code": 0
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/portgroup/associate/mappingview?TYPE=257&AS'
+                             'SOCIATEOBJTYPE=245&ASSOCIATEOBJID=1/GET'] = (
+    FACK_GET_PORTG_BY_VIEW)
+
+FACK_GET_PORT_BY_PORTG = """
+{
+    "data":[{
+        "CONFSPEED":"0","FCCONFMODE":"3",
+        "FCRUNMODE":"0","HEALTHSTATUS":"1","ID":"2000643e8c4c5f66",
+        "MAXSUPPORTSPEED":"16000","NAME":"P0","PARENTID":"0B.1",
+        "PARENTTYPE":209,"RUNNINGSTATUS":"10","RUNSPEED":"8000",
+        "WWN":"2000643e8c4c5f66"
+    }],
+    "error":{
+        "code":0,"description":"0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/fc_port/associate/portgroup?TYPE=212&ASSOCI'
+                             'ATEOBJTYPE=257&ASSOCIATEOBJID=0/GET'] = (
+    FACK_GET_PORT_BY_PORTG)
+
+FACK_GET_PORTG = """
+{
+    "data": {
+        "TYPE": 257,
+        "NAME": "OpenStack_PortGroup_1",
+        "DESCRIPTION": "Please DO NOT change thefollowing message: 0",
+        "ID": "0"
+    },
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/portgroup/0/GET'] = FACK_GET_PORTG
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/portgroup/0/PUT'] = FACK_GET_PORTG
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/port/associate/portgroup/POST'] = (
+    FACK_GET_PORT_BY_PORTG)
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/port/associate/portgroup?ID=0&TYPE=257&ASSOCIA'
+                             'TEOBJTYPE=212&ASSOCIATEOBJID=2000643e8c4c5f66/DE'
+                             'LETE'] = (
+    FAKE_COMMON_SUCCESS_RESPONSE)
+
+FAKE_CREATE_PORTG = """
+{
+    "data": {
+        "DESCRIPTION": "Please DO NOT change the following message: 0",
+        "ID": "0",
+        "NAME": "OpenStack_PortGroup_1",
+        "TYPE": 257
+    },
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/PortGroup/POST'] = FAKE_CREATE_PORTG
+
+
+FAKE_GET_PORTG_FROM_PORT = """
+{
+    "data": [{
+        "TYPE": 257,
+        "NAME": "OpenStack_PortGroup_1",
+        "DESCRIPTION": "PleaseDONOTchangethefollowingmessage: 0",
+        "ID": "0"
+    }],
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/portgroup/associate/fc_port?TYPE=257&ASSOCIA'
+                             'TEOBJTYPE=212&ASSOCIATEOBJID=1114368/GET'] = (
+    FAKE_GET_PORTG_FROM_PORT)
+
+FACK_GET_VIEW_BY_PORTG = """
+{
+    "data": [{
+        "ASSOCIATEOBJID": "0",
+        "COUNT": "0",
+        "ASSOCIATEOBJTYPE": "0",
+        "INBANDLUNWWN": "",
+        "FORFILESYSTEM": "false",
+        "ID": "2",
+        "ENABLEINBANDCOMMAND": "false",
+        "NAME": "OpenStack_Mapping_View_1",
+        "WORKMODE": "0",
+        "TYPE": 245,
+        "HOSTLUNID": "0",
+        "DESCRIPTION": ""
+    }],
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/mappingview/associate/portgroup?TYPE=245&ASS'
+                             'OCIATEOBJTYPE=257&ASSOCIATEOBJID=0/GET'] = (
+    FACK_GET_VIEW_BY_PORTG)
+
+FACK_GET_LUNG_BY_VIEW = """
+{
+    "data": [{
+        "TYPE": 256,
+        "NAME": "OpenStack_LunGroup_1",
+        "DESCRIPTION": "OpenStack_LunGroup_1",
+        "ID": "1"
+    }],
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/lungroup/associate/mappingview?TYPE=256&ASSO'
+                             'CIATEOBJTYPE=245&ASSOCIATEOBJID=2/GET'] = (
+    FACK_GET_LUNG_BY_VIEW)
+
+FAKE_LUN_COUNT_RESPONSE_1 = """
+{
+    "data":{
+        "COUNT":"2"
+    },
+    "error":{
+        "code":0,
+        "description":"0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/lun/count?TYPE=11&ASSOCIATEOB'
+                             'JTYPE=256&ASSOCIATEOBJID=1/GET'] = (
+    FAKE_LUN_COUNT_RESPONSE_1)
+
+FAKE_PORTS_IN_PG_RESPONSE = """
+{
+    "data": [{
+        "ID": "1114114",
+        "WWN": "2002643e8c4c5f66"
+    },
+    {
+        "ID": "1114113",
+        "WWN": "2001643e8c4c5f66"
+    }],
+    "error": {
+        "code": 0,
+        "description": "0"
+    }
+}
+"""
+
+MAP_COMMAND_TO_FAKE_RESPONSE['/fc_port/associate?TYPE=213&ASSOCIATEOBJTYPE='
+                             '257&ASSOCIATEOBJID=0/GET'] = (
+    FAKE_PORTS_IN_PG_RESPONSE)
+
 
 def Fake_sleep(time):
     pass
@@ -1530,7 +1723,7 @@ class FakeFCStorage(huawei_driver.HuaweiFCDriver):
 
     def __init__(self, configuration):
         self.configuration = configuration
-        self.fcsan_lookup_service = None
+        self.fcsan = None
         self.db = FakeDB()
         self.huawei_conf = FakeHuaweiConf(self.configuration, 'iSCSI')
 
@@ -2389,7 +2582,7 @@ class HuaweiFCDriverTestCase(test.TestCase):
     def test_get_volume_status(self):
 
         data = self.driver.get_volume_stats()
-        self.assertEqual('2.0.3', data['driver_version'])
+        self.assertEqual('2.0.4', data['driver_version'])
 
     def test_extend_volume(self):
 
@@ -2614,37 +2807,60 @@ class HuaweiFCDriverTestCase(test.TestCase):
                                     test_new_type, None, test_host)
         self.assertFalse(retype)
 
-    def test_build_ini_targ_map(self):
-
+    @mock.patch.object(rest_client.RestClient, 'get_all_engines',
+                       return_value=[{'NODELIST': '["0A","0B"]', 'ID': '0'}])
+    def test_build_ini_targ_map_engie_recorded(self, mock_engines):
         fake_lookup_service = FCSanLookupService()
-        fake_lookup_service.get_device_mapping_from_network = mock.Mock(
-            return_value=fake_fabric_mapping)
 
         zone_helper = fc_zone_helper.FCZoneHelper(
             fake_lookup_service, self.driver.client)
-        (tgt_port_wwns,
-         init_targ_map) = (zone_helper.build_ini_targ_map(
-             ['10000090fa0d6754']))
+        (tgt_wwns, portg_id, init_targ_map) = zone_helper.build_ini_targ_map(
+            ['10000090fa0d6754'], '1', '11')
         target_port_wwns = ['2000643e8c4c5f66']
-        ini_target_map = {'10000090fa0d6754': ['2000643e8c4c5f66']}
-        self.assertEqual(target_port_wwns, tgt_port_wwns)
-        self.assertEqual(ini_target_map, init_targ_map)
-
-    def test_filter_port_by_contr(self):
-
-        # Six ports in one fabric.
-        ports_in_fabric = ['1', '2', '3', '4', '5', '6']
-        # Ports 1,3,4,7 belonged to controller A
-        # Ports 2,5,8 belonged to controller B
-        # ports 6 belonged to controller C
-        total_port_contr_map = {'1': 'A', '3': 'A', '4': 'A', '7': 'A',
-                                '2': 'B', '5': 'B', '8': 'B',
-                                '6': 'C'}
-        zone_helper = fc_zone_helper.FCZoneHelper(None, None)
-        filtered_ports = zone_helper._filter_port_by_contr(
-            ports_in_fabric, total_port_contr_map)
-        expected_filtered_ports = ['1', '3', '2', '5', '6']
-        self.assertEqual(expected_filtered_ports, filtered_ports)
+        self.assertEqual(target_port_wwns, tgt_wwns)
+        self.assertEqual({}, init_targ_map)
+
+    @mock.patch.object(rest_client.RestClient, 'get_all_engines',
+                       return_value=[{'NODELIST': '["0A"]', 'ID': '0'},
+                                     {'NODELIST': '["0B"]', 'ID': '1'}])
+    def test_build_ini_targ_map_engie_not_recorded(self, mock_engines):
+        fake_lookup_service = FCSanLookupService()
+
+        zone_helper = fc_zone_helper.FCZoneHelper(
+            fake_lookup_service, self.driver.client)
+        (tgt_wwns, portg_id, init_targ_map) = zone_helper.build_ini_targ_map(
+            ['10000090fa0d6754'], '1', '11')
+        expected_wwns = ['2000643e8c4c5f66']
+        expected_map = {'10000090fa0d6754': ['2000643e8c4c5f66']}
+        self.assertEqual(expected_wwns, tgt_wwns)
+        self.assertEqual(expected_map, init_targ_map)
+
+    @mock.patch.object(rest_client.RestClient, 'get_all_engines',
+                       return_value=[{'NODELIST': '["0A", "0B"]', 'ID': '0'}])
+    def test_build_ini_targ_map_no_map(self, mock_engines):
+        fake_lookup_service = FCSanLookupService()
+
+        zone_helper = fc_zone_helper.FCZoneHelper(
+            fake_lookup_service, self.driver.client)
+        # Host with id '5' has no map on the array.
+        (tgt_wwns, portg_id, init_targ_map) = zone_helper.build_ini_targ_map(
+            ['10000090fa0d6754'], '5', '11')
+        expected_wwns = ['2000643e8c4c5f66']
+        expected_map = {'10000090fa0d6754': ['2000643e8c4c5f66']}
+        self.assertEqual(expected_wwns, tgt_wwns)
+        self.assertEqual(expected_map, init_targ_map)
+
+    def test_get_init_targ_map(self):
+        fake_lookup_service = FCSanLookupService()
+
+        zone_helper = fc_zone_helper.FCZoneHelper(
+            fake_lookup_service, self.driver.client)
+        (tgt_wwns, portg_id, init_targ_map) = zone_helper.get_init_targ_map(
+            ['10000090fa0d6754'], '1')
+        expected_wwns = ['2000643e8c4c5f66']
+        expected_map = {'10000090fa0d6754': ['2000643e8c4c5f66']}
+        self.assertEqual(expected_wwns, tgt_wwns)
+        self.assertEqual(expected_map, init_targ_map)
 
     def test_multi_resturls_success(self):
 
index 2618bce962a048cabe79c602a7db1bf2494cb6b9..acd5b3de859fe934d0751913ba9d82f04bd5c680 100644 (file)
@@ -26,10 +26,13 @@ FILE_SYSTEM_POOL_TYPE = '2'
 HOSTGROUP_PREFIX = 'OpenStack_HostGroup_'
 LUNGROUP_PREFIX = 'OpenStack_LunGroup_'
 MAPPING_VIEW_PREFIX = 'OpenStack_Mapping_View_'
+PORTGROUP_PREFIX = 'OpenStack_PortGroup_'
 QOS_NAME_PREFIX = 'OpenStack_'
+PORTGROUP_DESCRIP_PREFIX = "Please do NOT modify this. Engine ID: "
 ARRAY_VERSION = 'V300R003C00'
 FC_PORT_CONNECTED = '10'
 FC_INIT_ONLINE = '27'
+FC_PORT_MODE_FABRIC = '0'
 CAPACITY_UNIT = 1024.0 * 1024.0 * 2
 DEFAULT_WAIT_TIMEOUT = 3600 * 24 * 30
 DEFAULT_WAIT_INTERVAL = 5
@@ -54,6 +57,7 @@ THICK_LUNTYPE = 0
 THIN_LUNTYPE = 1
 MAX_HOSTNAME_LENGTH = 31
 MAX_VOL_DESCRIPTION = 170
+PORT_NUM_PER_CONTR = 2
 
 OS_TYPE = {'Linux': '0',
            'Windows': '1',
index 4f93c5e627ddd0e77b4a7b445a95a80b763326f7..66fb5aa41c57735122d3cdc0e46897babab29c6d 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import json
+
 from oslo_log import log as logging
 
+from cinder import exception
+from cinder.i18n import _
 from cinder.volume.drivers.huawei import constants
 
 LOG = logging.getLogger(__name__)
@@ -24,46 +28,205 @@ class FCZoneHelper(object):
     """FC zone helper for Huawei driver."""
 
     def __init__(self, fcsan_lookup_service, client):
-        self.fcsan_lookup_service = fcsan_lookup_service
+        self.fcsan = fcsan_lookup_service
         self.client = client
 
-    def _get_fc_port_contr_map(self):
-        port_list = []
-        port_contr_map = {}
+    def _get_fc_ports_info(self):
+        ports_info = {}
         data = self.client.get_fc_ports_on_array()
         for item in data:
             if item['RUNNINGSTATUS'] == constants.FC_PORT_CONNECTED:
-                port_list.append(item['WWN'])
                 location = item['PARENTID'].split('.')
-                port_contr_map[item['WWN']] = location[0][1]
-        return port_list, port_contr_map
-
-    def _filter_port_by_contr(self, ports_in_fabric, port_contr_map):
-        filtered_ports = []
-        for contr in constants.CONTROLLER_LIST:
-            found_port_per_contr = 0
-            for port in ports_in_fabric:
-                if port in port_contr_map and port_contr_map[port] == contr:
-                    filtered_ports.append(port)
-                    found_port_per_contr = found_port_per_contr + 1
-                    # We select two ports per every controller.
-                    if found_port_per_contr == 2:
-                        break
-        return filtered_ports
-
-    def build_ini_targ_map(self, wwns):
-        tgt_wwns = []
-        init_targ_map = {}
-        port_lists, port_contr_map = self._get_fc_port_contr_map()
-        ini_tgt_map = (self.fcsan_lookup_service.
-                       get_device_mapping_from_network(wwns, port_lists))
+                port_info = {}
+                port_info['id'] = item['ID']
+                port_info['contr'] = location[0]
+                port_info['bandwidth'] = item['RUNSPEED']
+                ports_info[item['WWN']] = port_info
+        return ports_info
+
+    def _count_port_weight(self, port, ports_info):
+        LOG.debug("Count weight for port: %s.", port)
+        portgs = self.client.get_portgs_by_portid(ports_info[port]['id'])
+        LOG.debug("Port %(port)s belongs to PortGroup %(portgs)s.",
+                  {"port": port, "portgs": portgs})
+        weight = 0
+        for portg in portgs:
+            views = self.client.get_views_by_portg(portg)
+            if not views:
+                LOG.debug("PortGroup %s doesn't belong to any view.", portg)
+                break
+
+            LOG.debug("PortGroup %(portg)s belongs to view %(views)s.",
+                      {"portg": portg, "views": views[0]})
+            # In fact, there is just one view for one port group.
+            lungroup = self.client.get_lungroup_by_view(views[0])
+            lun_num = self.client.get_lunnum_from_lungroup(lungroup)
+            ports_in_portg = self.client.get_ports_by_portg(portg)
+            LOG.debug("PortGroup %(portg)s contains ports: %(ports)s.",
+                      {"portg": portg, "ports": ports_in_portg})
+            total_bandwidth = 0
+            for port_pg in ports_in_portg:
+                if port_pg in ports_info:
+                    total_bandwidth += int(ports_info[port_pg]['bandwidth'])
+
+            LOG.debug("Total bandwidth for PortGroup %(portg)s is %(bindw)s.",
+                      {"portg": portg, "bindw": total_bandwidth})
+
+            if total_bandwidth:
+                weight += float(lun_num) / float(total_bandwidth)
+
+        return weight
 
+    def _get_weighted_ports_per_contr(self, ports, ports_info):
+        port_weight_map = {}
+        for port in ports:
+            port_weight_map[port] = self._count_port_weight(port, ports_info)
+
+        sorted_ports = sorted(port_weight_map.items(), key=lambda d: d[1])
+        weighted_ports = []
+        count = 0
+        for port in sorted_ports:
+            if count >= constants.PORT_NUM_PER_CONTR:
+                break
+            weighted_ports.append(port[0])
+            count += 1
+        return weighted_ports
+
+    def _get_weighted_ports(self, contr_port_map, ports_info, contrs):
+        weighted_ports = []
+        for contr in contrs:
+            if contr in contr_port_map:
+                weighted_ports_per_contr = self._get_weighted_ports_per_contr(
+                    contr_port_map[contr], ports_info)
+                LOG.debug("Selected ports %(ports)s on controller %(contr)s.",
+                          {"ports": weighted_ports_per_contr,
+                           "contr": contr})
+                weighted_ports.extend(weighted_ports_per_contr)
+        return weighted_ports
+
+    def _filter_by_fabric(self, wwns, ports):
+        """Filter FC ports and initiators connected to fabrics."""
+        ini_tgt_map = self.fcsan.get_device_mapping_from_network(wwns, ports)
+        fabric_connected_ports = []
+        fabric_connected_initiators = []
         for fabric in ini_tgt_map:
-            ports_in_fabric = ini_tgt_map[fabric]['target_port_wwn_list']
-            contr_filtered_ports = self._filter_port_by_contr(ports_in_fabric,
-                                                              port_contr_map)
-            tgt_wwns.extend(contr_filtered_ports)
-            for ini in ini_tgt_map[fabric]['initiator_port_wwn_list']:
-                init_targ_map[ini] = contr_filtered_ports
-
-        return (list(set(tgt_wwns)), init_targ_map)
+            fabric_connected_ports.extend(
+                ini_tgt_map[fabric]['target_port_wwn_list'])
+            fabric_connected_initiators.extend(
+                ini_tgt_map[fabric]['initiator_port_wwn_list'])
+
+        if not fabric_connected_ports:
+            msg = _("No FC port connected to fabric.")
+            raise exception.VolumeBackendAPIException(data=msg)
+        if not fabric_connected_initiators:
+            msg = _("No initiator connected to fabric.")
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        LOG.debug("Fabric connected ports: %(ports)s, "
+                  "Fabric connected initiators: %(initiators)s.",
+                  {'ports': fabric_connected_ports,
+                   'initiators': fabric_connected_initiators})
+        return fabric_connected_ports, fabric_connected_initiators
+
+    def build_ini_targ_map(self, wwns, host_id, lun_id):
+        lun_info = self.client.get_lun_info(lun_id)
+        lun_contr_id = lun_info['OWNINGCONTROLLER']
+        engines = self.client.get_all_engines()
+
+        for engine in engines:
+            contrs = json.loads(engine['NODELIST'])
+            engine_id = engine['ID']
+            if lun_contr_id in contrs:
+                LOG.debug("LUN %(lun_id)s belongs to engine %(engine_id)s.",
+                          {"lun_id": lun_id, "engine_id": engine_id})
+                break
+
+        # Check if there is already a port group in the view.
+        # If yes and have already considered the engine,
+        # we won't change anything about the port group and zone.
+        view_name = constants.MAPPING_VIEW_PREFIX + host_id
+        portg_name = constants.PORTGROUP_PREFIX + host_id
+        view_id = self.client.find_mapping_view(view_name)
+        portg_info = self.client.get_portgroup_by_view(view_id)
+        portg_id = portg_info[0]['ID'] if portg_info else None
+
+        init_targ_map = {}
+        if portg_id:
+            description = portg_info[0].get("DESCRIPTION", '')
+            engines = description.replace(constants.PORTGROUP_DESCRIP_PREFIX,
+                                          "")
+            engines = engines.split(',')
+            ports = self.client.get_fc_ports_by_portgroup(portg_id)
+            if engine_id in engines:
+                LOG.debug("Have already selected ports for engine %s, just "
+                          "use them.", engine_id)
+                return (list(ports.keys()), portg_id, init_targ_map)
+
+        # Filter initiators and ports that connected to fabrics.
+        ports_info = self._get_fc_ports_info()
+        (fabric_connected_ports, fabric_connected_initiators) = (
+            self._filter_by_fabric(wwns, ports_info.keys()))
+
+        # Build a controller->ports map for convenience.
+        contr_port_map = {}
+        for port in fabric_connected_ports:
+            contr = ports_info[port]['contr']
+            if not contr_port_map.get(contr):
+                contr_port_map[contr] = []
+            contr_port_map[contr].append(port)
+        LOG.debug("Controller port map: %s.", contr_port_map)
+
+        # Get the 'best' ports for the given controllers.
+        weighted_ports = self._get_weighted_ports(contr_port_map, ports_info,
+                                                  contrs)
+
+        # Handle port group.
+        port_list = [ports_info[port]['id'] for port in weighted_ports]
+
+        if portg_id:
+            # Add engine ID to the description of the port group.
+            self.client.append_portg_desc(portg_id, engine_id)
+            # Extend the weighted_ports to include the ports already in the
+            # port group.
+            weighted_ports.extend(list(ports.keys()))
+        else:
+            portg_id = self.client.get_tgt_port_group(portg_name)
+            if portg_id:
+                LOG.debug("Found port group %s not belonged to any view, "
+                          "deleting it.", portg_name)
+                ports = self.client.get_fc_ports_by_portgroup(portg_id)
+                for port_id in ports.values():
+                    self.client.remove_port_from_portgroup(portg_id, port_id)
+                self.client.delete_portgroup(portg_id)
+            description = constants.PORTGROUP_DESCRIP_PREFIX + engine_id
+            portg_id = self.client.create_portg(portg_name, description)
+
+        for port in port_list:
+            self.client.add_port_to_portg(portg_id, port)
+
+        for ini in fabric_connected_initiators:
+            init_targ_map[ini] = weighted_ports
+        LOG.debug("build_ini_targ_map: Port group name: %(portg_name)s, "
+                  "init_targ_map: %(map)s.",
+                  {"portg_name": portg_name,
+                   "map": init_targ_map})
+        return weighted_ports, portg_id, init_targ_map
+
+    def get_init_targ_map(self, wwns, host_id):
+        error_ret = ([], None, {})
+        if not host_id:
+            return error_ret
+
+        view_name = constants.MAPPING_VIEW_PREFIX + host_id
+        view_id = self.client.find_mapping_view(view_name)
+        if not view_id:
+            return error_ret
+        port_group = self.client.get_portgroup_by_view(view_id)
+        portg_id = port_group[0]['ID'] if port_group else None
+        ports = self.client.get_fc_ports_by_portgroup(portg_id)
+        for port_id in ports.values():
+            self.client.remove_port_from_portgroup(portg_id, port_id)
+        init_targ_map = {}
+        for wwn in wwns:
+            init_targ_map[wwn] = list(ports.keys())
+        return list(ports.keys()), portg_id, init_targ_map
index 276f0b2a6ea52778d48701df6ef71dfb4f507aa5..79530b52896cca610c173339c390c46e76c01914 100644 (file)
@@ -1451,13 +1451,14 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
         2.0.1 - Manage/unmanage volume support
         2.0.2 - Refactor HuaweiFCDriver
         2.0.3 - Manage/unmanage snapshot support
+        2.0.4 - Balanced FC port selection
     """
 
-    VERSION = "2.0.3"
+    VERSION = "2.0.4"
 
     def __init__(self, *args, **kwargs):
         super(HuaweiFCDriver, self).__init__(*args, **kwargs)
-        self.fcsan_lookup_service = None
+        self.fcsan = None
 
     def get_volume_stats(self, refresh=False):
         """Get volume status."""
@@ -1481,23 +1482,23 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
              'volume': volume_name},)
 
         lun_id = self.client.get_lun_id(volume, volume_name)
+        portg_id = None
 
         original_host_name = connector['host']
         host_name = huawei_utils.encode_host_name(original_host_name)
         host_id = self.client.add_host_with_check(host_name,
                                                   original_host_name)
 
-        if not self.fcsan_lookup_service:
-            self.fcsan_lookup_service = fczm_utils.create_lookup_service()
+        if not self.fcsan:
+            self.fcsan = fczm_utils.create_lookup_service()
 
-        if self.fcsan_lookup_service:
+        if self.fcsan:
             # Use FC switch.
-            host_id = self.client.add_host_with_check(
-                host_name, original_host_name)
-            zone_helper = fc_zone_helper.FCZoneHelper(
-                self.fcsan_lookup_service, self.client)
-            (tgt_port_wwns, init_targ_map) = (
-                zone_helper.build_ini_targ_map(wwns))
+            host_id = self.client.add_host_with_check(host_name,
+                                                      original_host_name)
+            zone_helper = fc_zone_helper.FCZoneHelper(self.fcsan, self.client)
+            (tgt_port_wwns, portg_id, init_targ_map) = (
+                zone_helper.build_ini_targ_map(wwns, host_id, lun_id))
             for ini in init_targ_map:
                 self.client.ensure_fc_initiator_added(ini, host_id)
         else:
@@ -1530,9 +1531,8 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
 
         # Add host into hostgroup.
         hostgroup_id = self.client.add_host_to_hostgroup(host_id)
-        map_info = self.client.do_mapping(lun_id,
-                                          hostgroup_id,
-                                          host_id)
+        map_info = self.client.do_mapping(lun_id, hostgroup_id,
+                                          host_id, portg_id)
         host_lun_id = self.client.get_host_lun_id(host_id, lun_id)
 
         # Return FC properties.
@@ -1654,15 +1654,16 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
             fc_info = {'driver_volume_type': 'fibre_channel',
                        'data': {}}
         else:
-            if not self.fcsan_lookup_service:
-                self.fcsan_lookup_service = fczm_utils.create_lookup_service()
+            portg_id = None
+            if not self.fcsan:
+                self.fcsan = fczm_utils.create_lookup_service()
 
-            if self.fcsan_lookup_service:
-                zone_helper = fc_zone_helper.FCZoneHelper(
-                    self.fcsan_lookup_service, self.client)
+            if self.fcsan:
+                zone_helper = fc_zone_helper.FCZoneHelper(self.fcsan,
+                                                          self.client)
 
-                (tgt_port_wwns, init_targ_map) = (
-                    zone_helper.build_ini_targ_map(wwns))
+                (tgt_port_wwns, portg_id, init_targ_map) = (
+                    zone_helper.get_init_targ_map(wwns, host_id))
             else:
                 (tgt_port_wwns, init_targ_map) = (
                     self.client.get_init_targ_map(wwns))
@@ -1676,6 +1677,12 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
                     self.client.delete_lungroup_mapping_view(view_id,
                                                              lungroup_id)
                 self.client.delete_lungroup(lungroup_id)
+            if portg_id:
+                if view_id and self.client.is_portgroup_associated_to_view(
+                        view_id, portg_id):
+                    self.client.delete_portgroup_mapping_view(view_id,
+                                                              portg_id)
+                    self.client.delete_portgroup(portg_id)
 
             if host_id:
                 hostgroup_name = constants.HOSTGROUP_PREFIX + host_id
index 495582c6465a3cc55fe297e860f50ec6520f2af7..12cd61e5bf63c9f395e505660dad59c2ec795b46 100644 (file)
@@ -922,13 +922,16 @@ class RestClient(object):
 
     def get_lunnum_from_lungroup(self, lungroup_id):
         """Check if there are still other luns associated to the lungroup."""
+        lunnum = 0
+        if not lungroup_id:
+            return lunnum
+
         url = ("/lun/count?TYPE=11&ASSOCIATEOBJTYPE=256&"
                "ASSOCIATEOBJID=%s" % lungroup_id)
         result = self.call(url, None, "GET")
         self._assert_rest_result(result, _('Find lun number error.'))
-        lunnum = -1
         if 'data' in result:
-            lunnum = result['data']['COUNT']
+            lunnum = int(result['data']['COUNT'])
         return lunnum
 
     def is_portgroup_associated_to_view(self, view_id, portgroup_id):
@@ -1885,3 +1888,129 @@ class RestClient(object):
                             rss_obj.get('LUNMirror') == 'TRUE'):
                         return True
         return False
+
+    def get_portgs_by_portid(self, port_id):
+        portgs = []
+        if not port_id:
+            return portgs
+        url = ("/portgroup/associate/fc_port?TYPE=257&ASSOCIATEOBJTYPE=212&"
+               "ASSOCIATEOBJID=%s") % port_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get port groups by port error.'))
+        for item in result.get("data", []):
+            portgs.append(item["ID"])
+        return portgs
+
+    def get_views_by_portg(self, portg_id):
+        views = []
+        if not portg_id:
+            return views
+        url = ("/mappingview/associate/portgroup?TYPE=245&ASSOCIATEOBJTYPE="
+               "257&ASSOCIATEOBJID=%s") % portg_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get views by port group error.'))
+        for item in result.get("data", []):
+            views.append(item["ID"])
+        return views
+
+    def get_lungroup_by_view(self, view_id):
+        if not view_id:
+            return None
+        url = ("/lungroup/associate/mappingview?TYPE=256&ASSOCIATEOBJTYPE="
+               "245&ASSOCIATEOBJID=%s") % view_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get LUN group by view error.'))
+        for item in result.get("data", []):
+            # In fact, there is just one lungroup in a view.
+            return item["ID"]
+
+    def get_portgroup_by_view(self, view_id):
+        if not view_id:
+            return None
+        url = ("/portgroup/associate/mappingview?TYPE=257&ASSOCIATEOBJTYPE="
+               "245&ASSOCIATEOBJID=%s") % view_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get port group by view error.'))
+        return result.get("data", [])
+
+    def get_fc_ports_by_portgroup(self, portg_id):
+        ports = {}
+        if not portg_id:
+            return ports
+        url = ("/fc_port/associate/portgroup?TYPE=212&ASSOCIATEOBJTYPE=257"
+               "&ASSOCIATEOBJID=%s") % portg_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get FC ports by port group '
+                                           'error.'))
+        for item in result.get("data", []):
+            ports[item["WWN"]] = item["ID"]
+        return ports
+
+    def create_portg(self, portg_name, description=""):
+        url = "/PortGroup"
+        data = {"DESCRIPTION": description,
+                "NAME": portg_name,
+                "TYPE": 257}
+        result = self.call(url, data, "POST")
+        self._assert_rest_result(result, _('Create port group error.'))
+        if "data" in result:
+            return result['data']['ID']
+
+    def add_port_to_portg(self, portg_id, port_id):
+        url = "/port/associate/portgroup"
+        data = {"ASSOCIATEOBJID": port_id,
+                "ASSOCIATEOBJTYPE": 212,
+                "ID": portg_id,
+                "TYPE": 257}
+        result = self.call(url, data, "POST")
+        self._assert_rest_result(result, _('Add port to port group error.'))
+
+    def delete_portgroup(self, portg_id):
+        url = "/PortGroup/%s" % portg_id
+        result = self.call(url, None, "DELETE")
+        self._assert_rest_result(result, _('Delete port group error.'))
+
+    def remove_port_from_portgroup(self, portg_id, port_id):
+        url = (("/port/associate/portgroup?ID=%(portg_id)s&TYPE=257&"
+               "ASSOCIATEOBJTYPE=212&ASSOCIATEOBJID=%(port_id)s")
+               % {"portg_id": portg_id, "port_id": port_id})
+        result = self.call(url, None, "DELETE")
+        self._assert_rest_result(result, _('Remove port from port group'
+                                           ' error.'))
+
+    def get_all_engines(self):
+        url = "/storageengine"
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get engines error.'))
+
+        return result.get("data", [])
+
+    def get_portg_info(self, portg_id):
+        url = "/portgroup/%s" % portg_id
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get port group error.'))
+
+        return result.get("data", {})
+
+    def append_portg_desc(self, portg_id, description):
+        portg_info = self.get_portg_info(portg_id)
+        new_description = portg_info.get('DESCRIPTION') + ',' + description
+        url = "/portgroup/%s" % portg_id
+        data = {"DESCRIPTION": new_description,
+                "ID": portg_id,
+                "TYPE": 257}
+        result = self.call(url, data, "PUT")
+        self._assert_rest_result(result, _('Append port group description'
+                                           ' error.'))
+
+    def get_ports_by_portg(self, portg_id):
+        wwns = []
+        url = ("/fc_port/associate?TYPE=213&ASSOCIATEOBJTYPE=257"
+               "&ASSOCIATEOBJID=%s" % portg_id)
+        result = self.call(url, None, "GET")
+
+        msg = _('Get ports by port group error.')
+        self._assert_rest_result(result, msg)
+        for item in result.get('data', []):
+            wwns.append(item['WWN'])
+        return wwns
diff --git a/releasenotes/notes/balanced-fc-port-selection-fbf6b841fea99156.yaml b/releasenotes/notes/balanced-fc-port-selection-fbf6b841fea99156.yaml
new file mode 100644 (file)
index 0000000..ea142c1
--- /dev/null
@@ -0,0 +1,2 @@
+features:
+  - Support balanced FC port selection for Huawei drivers.