]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add volume retype support for Huawei driver
authorzhongjun <jun.zhongjun@huawei.com>
Tue, 14 Jul 2015 10:06:51 +0000 (18:06 +0800)
committerWilson Liu <liuxinguo@huawei.com>
Mon, 24 Aug 2015 04:03:54 +0000 (12:03 +0800)
Add retype volume function for Huawei driver, including
SmartQos, SmartTier, SmartPartition and SmartCache,
SmartThin/Thick.

This commit adds the following qualified extra-specs:
  * capabilities:smarttier='<is> True'
    smarttier:policy='X'(X:0,1,2,3)"(default value is 1)
  * capabilities:smartpartition='<is> True'
    smartpartition:partitionname=test_partition_name
  * capabilities:smartcache='<is> True'
    smartcache:cachename=test_cache_name
  * capabilities:thin_provisioning='<is> True'
  * capabilities:thick_provisioning='<is> True'

Implements: blueprint huawei-driver-support-retype
Change-Id: I6bd1d39aca5a368594ca39c75c86eaf6ab20b00c

cinder/tests/unit/test_huawei_drivers.py
cinder/volume/drivers/huawei/constants.py
cinder/volume/drivers/huawei/huawei_driver.py
cinder/volume/drivers/huawei/huawei_utils.py
cinder/volume/drivers/huawei/rest_client.py

index c327a2854a24c52e8c2c6e801e0cd2551a39676c..5c1496c99fe8c20ac6a8ab05b9537ac025044b2e 100644 (file)
@@ -97,6 +97,28 @@ test_host = {'host': 'ubuntu001@backend001#OpenStack_Pool',
                               }
              }
 
+test_new_type = {
+    'name': u'new_type',
+    'qos_specs_id': None,
+    'deleted': False,
+    'created_at': None,
+    'updated_at': None,
+    'extra_specs': {
+        'smarttier': '<is> true',
+        'smartcache': '<is> true',
+        'smartpartition': '<is> true',
+        'thin_provisioning': '<is> true',
+        'thick_provisioning': '<is> False',
+        'policy': '2',
+        'smartcache:cachename': 'cache-test',
+        'smartpartition:partitionname': 'partition-test',
+    },
+    'is_public': True,
+    'deleted_at': None,
+    'id': u'530a56e1-a1a4-49f3-ab6c-779a6e5d999f',
+    'description': None,
+}
+
 FakeConnector = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3',
                  'wwpns': ['10000090fa0d6754'],
                  'wwnns': ['10000090fa0d6755'],
@@ -696,6 +718,45 @@ FAKE_PORT_GROUP_RESPONSE = """
 }
 """
 
+FAKE_ISCSI_INITIATOR_RESPONSE = """
+{
+    "error":{
+        "code": 0
+    },
+    "data":[{
+        "CHAPNAME": "mm-user",
+        "HEALTHSTATUS": "1",
+        "ID": "iqn.1993-08.org.debian:01:9073aba6c6f",
+        "ISFREE": "true",
+        "MULTIPATHTYPE": "1",
+        "NAME": "",
+        "OPERATIONSYSTEM": "255",
+        "RUNNINGSTATUS": "28",
+        "TYPE": 222,
+        "USECHAP": "true"
+    }]
+}
+"""
+
+FAKE_ISCSI_INITIATOR_RESPONSE = """
+{
+    "error":{
+        "code":0
+    },
+    "data":[{
+        "CHAPNAME":"mm-user",
+        "HEALTHSTATUS":"1",
+        "ID":"iqn.1993-08.org.debian:01:9073aba6c6f",
+        "ISFREE":"true",
+        "MULTIPATHTYPE":"1",
+        "NAME":"",
+        "OPERATIONSYSTEM":"255",
+        "RUNNINGSTATUS":"28",
+        "TYPE":222,
+        "USECHAP":"true"
+    }]
+}
+"""
 
 FAKE_ERROR_INFO_RESPONSE = """
 {
@@ -969,6 +1030,9 @@ MAP_COMMAND_TO_FAKE_RESPONSE['portgroup?range=[0-8191]&TYPE=257/GET'] = (
 MAP_COMMAND_TO_FAKE_RESPONSE['system/'] = (
     FAKE_SYSTEM_VERSION_RESPONSE)
 
+MAP_COMMAND_TO_FAKE_RESPONSE['lun/associate/cachepartition/POST'] = (
+    FAKE_SYSTEM_VERSION_RESPONSE)
+
 
 def Fake_sleep(time):
     pass
@@ -984,6 +1048,8 @@ class Fake18000Client(rest_client.RestClient):
         self.test_fail = False
         self.checkFlag = False
         self.remove_chap_flag = False
+        self.cache_not_exist = False
+        self.partition_not_exist = False
 
     def _change_file_mode(self, filepath):
         pass
@@ -1015,12 +1081,13 @@ class Fake18000Client(rest_client.RestClient):
         return True
 
     def get_partition_id_by_name(self, name):
+        if self.partition_not_exist:
+            return None
         return "11"
 
-    def add_lun_to_partition(self, lunid, partition_id):
-        pass
-
     def get_cache_id_by_name(self, name):
+        if self.cache_not_exist:
+            return None
         return "11"
 
     def add_lun_to_cache(self, lunid, cache_id):
@@ -1719,6 +1786,35 @@ class Huawei18000FCDriverTestCase(test.TestCase):
         self.assertEqual({'_name_id': '21ec7341-9256-497b-97d9-ef48edcf0637'},
                          model_update)
 
+    def test_retype_volume_success(self):
+        self.driver.restclient.login()
+        retype = self.driver.retype(None, test_volume,
+                                    test_new_type, None, test_host)
+        self.assertTrue(retype)
+
+    def test_retype_volume_cache_fail(self):
+        self.driver.restclient.cache_not_exist = True
+        self.driver.restclient.login()
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.retype, None,
+                          test_volume, test_new_type, None, test_host)
+
+    def test_retype_volume_partition_fail(self):
+        self.driver.restclient.partition_not_exist = True
+        self.driver.restclient.login()
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.retype, None,
+                          test_volume, test_new_type, None, test_host)
+
+    @mock.patch.object(rest_client.RestClient, 'add_lun_to_partition')
+    def test_retype_volume_fail(self, mock_add_lun_to_partition):
+        self.driver.restclient.login()
+        mock_add_lun_to_partition.side_effect = (
+            exception.VolumeBackendAPIException(data='Error occurred.'))
+        retype = self.driver.retype(None, test_volume,
+                                    test_new_type, None, test_host)
+        self.assertFalse(retype)
+
     def create_fake_conf_file(self):
         """Create a fake Config file
 
index 3def14a581ca84661a31630df41defb972273d96..5bf2a74c41681a8c35df1962d0e573ee3ad2adce 100644 (file)
@@ -39,6 +39,8 @@ ERROR_CONNECT_TO_SERVER = -403
 ERROR_UNAUTHORIZED_TO_SERVER = -401
 SOCKET_TIME_OUT = 720
 
+THICK_LUNTYPE = 0
+THIN_LUNTYPE = 1
 MAX_HOSTNAME_LENGTH = 31
 
 OS_TYPE = {'Linux': '0',
@@ -52,3 +54,5 @@ OS_TYPE = {'Linux': '0',
 
 HUAWEI_VALID_KEYS = ['maxIOPS', 'minIOPS', 'minBandWidth',
                      'maxBandWidth', 'latency', 'IOType']
+QOS_KEYS = ['MAXIOPS', 'MINIOPS', 'MINBANDWidth',
+            'MAXBANDWidth', 'LATENCY', 'IOTYPE']
index 3e1f94580c8e0e24277c68b8ccb6ffcbd77ce2d1..0b63dcd31a524b7d1d9c04fe812b21ba37218f90 100644 (file)
@@ -45,7 +45,7 @@ CONF = cfg.CONF
 CONF.register_opts(huawei_opt)
 
 
-class HuaweiBaseDriver(driver.MigrateVD, driver.BaseVD):
+class HuaweiBaseDriver(driver.VolumeDriver):
 
     def __init__(self, *args, **kwargs):
         super(HuaweiBaseDriver, self).__init__(*args, **kwargs)
@@ -747,6 +747,220 @@ class HuaweiBaseDriver(driver.MigrateVD, driver.BaseVD):
             self.restclient.remove_host(host_id)
             self.restclient.delete_mapping_view(view_id)
 
+    def retype(self, ctxt, volume, new_type, diff, host):
+        """Convert the volume to be of the new type."""
+        LOG.debug("Enter retype: id=%(id)s, new_type=%(new_type)s, "
+                  "diff=%(diff)s, host=%(host)s.", {'id': volume['id'],
+                                                    'new_type': new_type,
+                                                    'diff': diff,
+                                                    'host': host})
+
+        # Check what changes are needed
+        migration, change_opts, lun_id = self.determine_changes_when_retype(
+            volume, new_type, host)
+
+        try:
+            if migration:
+                LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with "
+                          "change %(change_opts)s.",
+                          {"lun_id": lun_id, "change_opts": change_opts})
+                if self._migrate_volume(volume, host, new_type):
+                    return True
+                else:
+                    LOG.warning(_LW("Storage-assisted migration failed during "
+                                    "retype."))
+                    return False
+            else:
+                # Modify lun to change policy
+                self.modify_lun(lun_id, change_opts)
+                return True
+        except exception.VolumeBackendAPIException:
+            LOG.exception(_LE('Retype volume error.'))
+            return False
+
+    def modify_lun(self, lun_id, change_opts):
+        if change_opts.get('partitionid', None):
+            old, new = change_opts['partitionid']
+            old_id = old[0]
+            old_name = old[1]
+            new_id = new[0]
+            new_name = new[1]
+            if old_id:
+                self.restclient.remove_lun_from_partition(lun_id, old_id)
+            if new_id:
+                self.restclient.add_lun_to_partition(lun_id, new_id)
+            LOG.info(_LI("Retype LUN(id: %(lun_id)s) smartpartition from "
+                         "(name: %(old_name)s, id: %(old_id)s) to "
+                         "(name: %(new_name)s, id: %(new_id)s) success."),
+                     {"lun_id": lun_id,
+                      "old_id": old_id, "old_name": old_name,
+                      "new_id": new_id, "new_name": new_name})
+
+        if change_opts.get('cacheid', None):
+            old, new = change_opts['cacheid']
+            old_id = old[0]
+            old_name = old[1]
+            new_id = new[0]
+            new_name = new[1]
+            if old_id:
+                self.restclient.remove_lun_from_cache(lun_id, old_id)
+            if new_id:
+                self.restclient.add_lun_to_cache(lun_id, new_id)
+            LOG.info(_LI("Retype LUN(id: %(lun_id)s) smartcache from "
+                         "(name: %(old_name)s, id: %(old_id)s) to "
+                         "(name: %(new_name)s, id: %(new_id)s) successfully."),
+                     {'lun_id': lun_id,
+                      'old_id': old_id, "old_name": old_name,
+                      'new_id': new_id, "new_name": new_name})
+
+        if change_opts.get('policy', None):
+            old_policy, new_policy = change_opts['policy']
+            self.restclient.change_lun_smarttier(lun_id, new_policy)
+            LOG.info(_LI("Retype LUN(id: %(lun_id)s) smarttier policy from "
+                         "%(old_policy)s to %(new_policy)s success."),
+                     {'lun_id': lun_id,
+                      'old_policy': old_policy,
+                      'new_policy': new_policy})
+
+        if change_opts.get('qos', None):
+            old_qos, new_qos = change_opts['qos']
+            old_qos_id = old_qos[0]
+            old_qos_value = old_qos[1]
+            if old_qos_id:
+                self.remove_qos_lun(lun_id, old_qos_id)
+            if new_qos:
+                smart_qos = smartx.SmartQos(self.restclient)
+                smart_qos.create_qos(new_qos, lun_id)
+            LOG.info(_LI("Retype LUN(id: %(lun_id)s) smartqos from "
+                         "%(old_qos_value)s to %(new_qos)s success."),
+                     {'lun_id': lun_id,
+                      'old_qos_value': old_qos_value,
+                      'new_qos': new_qos})
+
+    def get_lun_specs(self, lun_id):
+        lun_opts = {
+            'policy': None,
+            'partitionid': None,
+            'cacheid': None,
+            'LUNType': None,
+        }
+
+        lun_info = self.restclient.get_lun_info(lun_id)
+        lun_opts['LUNType'] = int(lun_info['ALLOCTYPE'])
+        if lun_info['DATATRANSFERPOLICY']:
+            lun_opts['policy'] = lun_info['DATATRANSFERPOLICY']
+        if lun_info['SMARTCACHEPARTITIONID']:
+            lun_opts['cacheid'] = lun_info['SMARTCACHEPARTITIONID']
+        if lun_info['CACHEPARTITIONID']:
+            lun_opts['partitionid'] = lun_info['CACHEPARTITIONID']
+
+        return lun_opts
+
+    def determine_changes_when_retype(self, volume, new_type, host):
+        migration = False
+        change_opts = {
+            'policy': None,
+            'partitionid': None,
+            'cacheid': None,
+            'qos': None,
+            'host': None,
+            'LUNType': None,
+        }
+
+        lun_id = volume.get('provider_location', None)
+        old_opts = self.get_lun_specs(lun_id)
+
+        new_specs = new_type['extra_specs']
+        new_opts = huawei_utils._get_extra_spec_value(new_specs)
+        new_opts = smartx.SmartX().get_smartx_specs_opts(new_opts)
+
+        if 'LUNType' not in new_opts:
+            new_opts['LUNType'] = huawei_utils.find_luntype_in_xml(
+                self.xml_file_path)
+
+        if volume['host'] != host['host']:
+            migration = True
+            change_opts['host'] = (volume['host'], host['host'])
+        if old_opts['LUNType'] != new_opts['LUNType']:
+            migration = True
+            change_opts['LUNType'] = (old_opts['LUNType'], new_opts['LUNType'])
+
+        new_cache_id = None
+        new_cache_name = new_opts['cachename']
+        if new_cache_name:
+            new_cache_id = self.restclient.get_cache_id_by_name(new_cache_name)
+            if new_cache_id is None:
+                msg = (_(
+                    "Can't find cache name on the array, cache name is: "
+                    "%(name)s.") % {'name': new_cache_name})
+                LOG.error(msg)
+                raise exception.VolumeBackendAPIException(data=msg)
+
+        new_partition_id = None
+        new_partition_name = new_opts['partitionname']
+        if new_partition_name:
+            new_partition_id = self.restclient.get_partition_id_by_name(
+                new_partition_name)
+            if new_partition_id is None:
+                msg = (_(
+                    "Can't find partition name on the array, partition name "
+                    "is: %(name)s.") % {'name': new_partition_name})
+                LOG.error(msg)
+                raise exception.VolumeBackendAPIException(data=msg)
+
+        # smarttier
+        if old_opts['policy'] != new_opts['policy']:
+            change_opts['policy'] = (old_opts['policy'], new_opts['policy'])
+
+        # smartcache
+        old_cache_id = old_opts['cacheid']
+        if old_cache_id != new_cache_id:
+            old_cache_name = None
+            if old_cache_id:
+                cache_info = self.restclient.get_cache_info_by_id(old_cache_id)
+                old_cache_name = cache_info['NAME']
+            change_opts['cacheid'] = ([old_cache_id, old_cache_name],
+                                      [new_cache_id, new_cache_name])
+
+        # smartpartition
+        old_partition_id = old_opts['partitionid']
+        if old_partition_id != new_partition_id:
+            old_partition_name = None
+            if old_partition_id:
+                partition_info = self.restclient.get_partition_info_by_id(
+                    old_partition_id)
+                old_partition_name = partition_info['NAME']
+            change_opts['partitionid'] = ([old_partition_id,
+                                           old_partition_name],
+                                          [new_partition_id,
+                                           new_partition_name])
+
+        # smartqos
+        new_qos = huawei_utils.get_qos_by_volume_type(new_type)
+        old_qos_id = self.restclient.get_qosid_by_lunid(lun_id)
+        old_qos = self._get_qos_specs_from_array(old_qos_id)
+        if old_qos != new_qos:
+            change_opts['qos'] = ([old_qos_id, old_qos], new_qos)
+
+        LOG.debug("Determine changes when retype. Migration: "
+                  "%(migration)s, change_opts: %(change_opts)s.",
+                  {'migration': migration, 'change_opts': change_opts})
+        return migration, change_opts, lun_id
+
+    def _get_qos_specs_from_array(self, qos_id):
+        qos = {}
+        qos_info = {}
+        if qos_id:
+            qos_info = self.restclient.get_qos_info(qos_id)
+
+        for key, value in qos_info.items():
+            if key.upper() in constants.QOS_KEYS:
+                if key.upper() == 'LATENCY' and value == '0':
+                    continue
+                else:
+                    qos[key.upper()] = value
+        return qos
+
     def terminate_connection_fc(self, volume, connector):
         """Delete map between a volume and a host."""
         wwns = connector['wwpns']
@@ -867,6 +1081,7 @@ class Huawei18000ISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver):
                 ISCSI multipath support
                 SmartX support
                 Volume migration support
+                Volume retype support
     """
 
     VERSION = "1.1.1"
@@ -905,6 +1120,7 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
                 Multiple pools support
                 SmartX support
                 Volume migration support
+                Volume retype support
     """
 
     VERSION = "1.1.1"
index e2de141d178e75ba3648e753118b6643751a3237..f601c1d3a14e4bbf893fd4387ebf6c4cb14ebcdb 100644 (file)
@@ -113,12 +113,13 @@ def _get_opts_from_specs(opts_capability, opts_value, specs):
             words = value.split()
 
             if not (words and len(words) == 2 and words[0] == '<is>'):
-                LOG.error(_LE("Capabilities value must be specified as "
-                              "'<is> True' or '<is> true'."))
+                LOG.error(_LE("Extra specs must be specified as "
+                              "capabilities:%s='<is> True' or "
+                              "'<is> true'."), key)
             else:
-                del words[0]
-                value = words[0]
-                opts[key] = value.lower()
+                # Get the second value(true/True) of the Extra specs
+                # value(<is> true/<is> True)
+                opts[key] = words[1].lower()
 
         if (scope in opts_capability) and (key in opts_value):
             if (scope in opts_associate) and (opts_associate[scope] == key):
@@ -329,9 +330,9 @@ def find_luntype_in_xml(xml_file_path):
     if luntype:
         if luntype.strip() in ['Thick', 'Thin']:
             if luntype.strip() == 'Thick':
-                luntype = 0
+                luntype = constants.THICK_LUNTYPE
             elif luntype.strip() == 'Thin':
-                luntype = 1
+                luntype = constants.THIN_LUNTYPE
         else:
             err_msg = (_(
                 "LUNType config is wrong. LUNType must be 'Thin'"
@@ -340,7 +341,7 @@ def find_luntype_in_xml(xml_file_path):
             LOG.error(err_msg)
             raise exception.VolumeBackendAPIException(data=err_msg)
     else:
-        luntype = 0
+        luntype = constants.THICK_LUNTYPE
     return luntype
 
 
index ff7bd2dcd57afd062a1a4eb61f340cb377dba3d3..812a4d57972b19b367b424efda6ec97f74d7125e 100644 (file)
@@ -1287,6 +1287,17 @@ class RestClient(object):
         result = self.call(url, data, "PUT")
         self._assert_rest_result(result, _('Change lun priority error.'))
 
+    def change_lun_smarttier(self, lunid, smarttier_policy):
+        """Change lun smarttier policy."""
+        url = self.url + "/lun/" + lunid
+        data = json.dumps({"TYPE": "11",
+                           "ID": lunid,
+                           "DATATRANSFERPOLICY": smarttier_policy})
+
+        result = self.call(url, data, "PUT")
+        self._assert_rest_result(
+            result, _('Change lun smarttier policy error.'))
+
     def get_qosid_by_lunid(self, lun_id):
         """Get QoS id by lun id."""
         url = self.url + "/lun/" + lun_id
@@ -1396,6 +1407,13 @@ class RestClient(object):
         result = self.call(url, data, "POST")
         self._assert_rest_result(result, _('Add lun to partition error.'))
 
+    def remove_lun_from_partition(self, lun_id, partition_id):
+        url = (self.url + '/lun/associate/cachepartition?ID=' + partition_id
+               + '&ASSOCIATEOBJTYPE=11&ASSOCIATEOBJID=' + lun_id)
+
+        result = self.call(url, None, "DELETE")
+        self._assert_rest_result(result, _('Remove lun from partition error.'))
+
     def get_cache_id_by_name(self, name):
         url = self.url + "/SMARTCACHEPARTITION"
         result = self.call(url, None, "GET")
@@ -1406,6 +1424,27 @@ class RestClient(object):
                 if name == item['NAME']:
                     return item['ID']
 
+    def get_cache_info_by_id(self, cacheid):
+        url = self.url + "/SMARTCACHEPARTITION/" + cacheid
+        data = json.dumps({"TYPE": "273",
+                           "ID": cacheid})
+
+        result = self.call(url, data, "GET")
+        self._assert_rest_result(
+            result, _('Get smartcache by cache id error.'))
+
+        return result['data']
+
+    def remove_lun_from_cache(self, lun_id, cache_id):
+        url = self.url + "/SMARTCACHEPARTITION/REMOVE_ASSOCIATE"
+        data = json.dumps({"ID": cache_id,
+                           "ASSOCIATEOBJTYPE": 11,
+                           "ASSOCIATEOBJID": lun_id,
+                           "TYPE": 273})
+
+        result = self.call(url, data, "PUT")
+        self._assert_rest_result(result, _('Remove lun from cache error.'))
+
     def find_available_qos(self, qos):
         """"Find available QoS on the array."""
         qos_id = None
@@ -1431,6 +1470,7 @@ class RestClient(object):
         return (qos_id, lun_list)
 
     def add_lun_to_qos(self, qos_id, lun_id, lun_list):
+        """Add lun to QoS."""
         url = self.url + "/ioclass/" + qos_id
         lun_list = []
         lun_string = lun_list[1:-1]