]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp eseries implementation for manage/unmanage
authorNavneet Singh <singn@netapp.com>
Thu, 28 Aug 2014 18:39:17 +0000 (00:09 +0530)
committerBob Callaway <bob.callaway@netapp.com>
Thu, 26 Feb 2015 17:07:45 +0000 (17:07 +0000)
This patch enables manage and unmanage support
for the eseries iscsi driver.

Implements: Blueprint eseries-manage-unmanage

Change-Id: I5a7f090300065d829bc94c81d8b976dcb541b2a0

cinder/tests/test_netapp_eseries_iscsi.py
cinder/volume/drivers/netapp/eseries/iscsi.py

index 2eab6c9a05562bac981d1cab262471d3f82d2fa3..a52c5f6aa4b14b4fe811802aea145e91a4592810 100644 (file)
@@ -1,7 +1,7 @@
 # Copyright (c) 2014 NetApp, Inc.
 # Copyright (c) 2015 Alex Meade.  All Rights Reserved.
 # Copyright (c) 2015 Rushil Chugh.  All Rights Reserved.
-# All Rights Reserved.
+# Copyright (c) 2015 Navneet Singh.  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -634,6 +634,9 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase):
     connector = {'initiator': 'iqn.1998-01.com.vmware:localhost-28a58148'}
     fake_size_gb = volume['size']
     fake_eseries_pool_label = 'DDP'
+    fake_ref = {'source-name': 'CFDGJSLS'}
+    fake_ret_vol = {'id': 'vol_id', 'label': 'label',
+                    'worldWideName': 'wwn', 'capacity': '2147583648'}
 
     def setUp(self):
         super(NetAppEseriesISCSIDriverTestCase, self).setUp()
@@ -724,7 +727,6 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase):
         self.assertEqual(info['driver_volume_type'], 'iscsi')
         properties = info.get('data')
         self.assertIsNotNone(properties, 'Target portal is none')
-        self.driver.terminate_connection(self.volume, self.connector)
         self.driver.delete_volume(self.volume)
 
     def test_map_already_mapped_diff_host(self):
@@ -1057,3 +1059,113 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase):
         driver = common.NetAppDriver(configuration=configuration)
         self.assertRaises(exception.NoValidHost,
                           driver._check_mode_get_or_register_storage_system)
+
+    def test_get_vol_with_label_wwn_missing(self):
+        self.assertRaises(exception.InvalidInput,
+                          self.driver._get_volume_with_label_wwn,
+                          None, None)
+
+    def test_get_vol_with_label_wwn_found(self):
+        fake_vl_list = [{'volumeRef': '1', 'volumeUse': 'standardVolume',
+                         'label': 'l1', 'volumeGroupRef': 'g1',
+                         'worlWideName': 'w1ghyu'},
+                        {'volumeRef': '2', 'volumeUse': 'standardVolume',
+                         'label': 'l2', 'volumeGroupRef': 'g2',
+                         'worldWideName': 'w2ghyu'}]
+        self.driver._objects['disk_pool_refs'] = ['g2', 'g3']
+        self.driver._client.list_volumes = mock.Mock(return_value=fake_vl_list)
+        vol = self.driver._get_volume_with_label_wwn('l2', 'w2:gh:yu')
+        self.assertEqual(1, self.driver._client.list_volumes.call_count)
+        self.assertEqual('2', vol['volumeRef'])
+
+    def test_get_vol_with_label_wwn_unmatched(self):
+        fake_vl_list = [{'volumeRef': '1', 'volumeUse': 'standardVolume',
+                         'label': 'l1', 'volumeGroupRef': 'g1',
+                         'worlWideName': 'w1ghyu'},
+                        {'volumeRef': '2', 'volumeUse': 'standardVolume',
+                         'label': 'l2', 'volumeGroupRef': 'g2',
+                         'worldWideName': 'w2ghyu'}]
+        self.driver._objects['disk_pool_refs'] = ['g2', 'g3']
+        self.driver._client.list_volumes = mock.Mock(return_value=fake_vl_list)
+        self.assertRaises(KeyError, self.driver._get_volume_with_label_wwn,
+                          'l2', 'abcdef')
+        self.assertEqual(1, self.driver._client.list_volumes.call_count)
+
+    def test_manage_existing_get_size(self):
+        self.driver._get_existing_vol_with_manage_ref = mock.Mock(
+            return_value=self.fake_ret_vol)
+        size = self.driver.manage_existing_get_size(self.volume, self.fake_ref)
+        self.assertEqual(3, size)
+        self.driver._get_existing_vol_with_manage_ref.assert_called_once_with(
+            self.volume, self.fake_ref)
+
+    def test_get_exist_vol_source_name_missing(self):
+        self.assertRaises(exception.ManageExistingInvalidReference,
+                          self.driver._get_existing_vol_with_manage_ref,
+                          self.volume, {'id': '1234'})
+
+    def test_get_exist_vol_source_not_found(self):
+        def _get_volume(v_id, v_name):
+            d = {'id': '1'}
+            return d[v_id]
+
+        self.driver._get_volume_with_label_wwn = mock.Mock(wraps=_get_volume)
+        self.assertRaises(exception.ManageExistingInvalidReference,
+                          self.driver._get_existing_vol_with_manage_ref,
+                          {'id': 'id2'}, {'source-name': 'name2'})
+        self.driver._get_volume_with_label_wwn.assert_called_once_with(
+            'name2', None)
+
+    def test_get_exist_vol_with_manage_ref(self):
+        fake_ret_vol = {'id': 'right'}
+        self.driver._get_volume_with_label_wwn = mock.Mock(
+            return_value=fake_ret_vol)
+        actual_vol = self.driver._get_existing_vol_with_manage_ref(
+            {'id': 'id2'}, {'source-name': 'name2'})
+        self.driver._get_volume_with_label_wwn.assert_called_once_with(
+            'name2', None)
+        self.assertEqual(fake_ret_vol, actual_vol)
+
+    @mock.patch.object(utils, 'convert_uuid_to_es_fmt')
+    def test_manage_existing_same_label(self, mock_convert_es_fmt):
+        self.driver._get_existing_vol_with_manage_ref = mock.Mock(
+            return_value=self.fake_ret_vol)
+        mock_convert_es_fmt.return_value = 'label'
+        self.driver._del_volume_frm_cache = mock.Mock()
+        self.driver._cache_volume = mock.Mock()
+        self.driver.manage_existing(self.volume, self.fake_ref)
+        self.driver._get_existing_vol_with_manage_ref.assert_called_once_with(
+            self.volume, self.fake_ref)
+        mock_convert_es_fmt.assert_called_once_with(
+            '114774fb-e15a-4fae-8ee2-c9723e3645ef')
+        self.assertEqual(0, self.driver._del_volume_frm_cache.call_count)
+        self.driver._cache_volume.assert_called_once_with(self.fake_ret_vol)
+
+    @mock.patch.object(utils, 'convert_uuid_to_es_fmt')
+    def test_manage_existing_new(self, mock_convert_es_fmt):
+        self.driver._get_existing_vol_with_manage_ref = mock.Mock(
+            return_value=self.fake_ret_vol)
+        mock_convert_es_fmt.return_value = 'vol_label'
+        self.driver._del_volume_frm_cache = mock.Mock()
+        self.driver._client.update_volume = mock.Mock(
+            return_value={'id': 'update', 'worldWideName': 'wwn'})
+        self.driver._cache_volume = mock.Mock()
+        self.driver.manage_existing(self.volume, self.fake_ref)
+        self.driver._get_existing_vol_with_manage_ref.assert_called_once_with(
+            self.volume, self.fake_ref)
+        mock_convert_es_fmt.assert_called_once_with(
+            '114774fb-e15a-4fae-8ee2-c9723e3645ef')
+        self.driver._client.update_volume.assert_called_once_with(
+            'vol_id', 'vol_label')
+        self.driver._del_volume_frm_cache.assert_called_once_with(
+            'label')
+        self.driver._cache_volume.assert_called_once_with(
+            {'id': 'update', 'worldWideName': 'wwn'})
+
+    @mock.patch.object(iscsi.LOG, 'info')
+    def test_unmanage(self, log_info):
+        self.driver._get_volume = mock.Mock(return_value=self.fake_ret_vol)
+        self.driver.unmanage(self.volume)
+        self.driver._get_volume.assert_called_once_with(
+            '114774fb-e15a-4fae-8ee2-c9723e3645ef')
+        self.assertEqual(1, log_info.call_count)
index 9ba6dadc8558c0c965bbe59a4f9ebb459e593225..5e5712229b0d3d7ddfaf2b1ae7634123e09060b4 100644 (file)
@@ -1,6 +1,7 @@
 # Copyright (c) 2014 NetApp, Inc.  All Rights Reserved.
 # Copyright (c) 2015 Alex Meade.  All Rights Reserved.
 # Copyright (c) 2015 Rushil Chugh.  All Rights Reserved.
+# Copyright (c) 2015 Navneet Singh.  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -18,6 +19,7 @@ iSCSI driver for NetApp E-series storage systems.
 """
 
 import copy
+import math
 import socket
 import time
 import uuid
@@ -89,6 +91,7 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
         'sata': 'SATA',
     }
     SSC_UPDATE_INTERVAL = 60  # seconds
+    WORLDWIDENAME = 'worldWideName'
 
     def __init__(self, *args, **kwargs):
         super(NetAppEseriesISCSIDriver, self).__init__(*args, **kwargs)
@@ -98,8 +101,8 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
             na_opts.netapp_connection_opts)
         self.configuration.append_config_values(na_opts.netapp_transport_opts)
         self.configuration.append_config_values(na_opts.netapp_eseries_opts)
-        self._backend_name = self.configuration.safe_get("volume_backend_name")\
-            or "NetApp_ESeries"
+        self._backend_name = self.configuration.safe_get(
+            "volume_backend_name") or "NetApp_ESeries"
         self._objects = {'disk_pool_refs': [], 'pools': [],
                          'volumes': {'label_ref': {}, 'ref_vol': {}},
                          'snapshots': {'label_ref': {}, 'ref_snap': {}}}
@@ -250,7 +253,9 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
     def _cache_volume(self, obj):
         """Caches volumes for further reference."""
         if (obj.get('volumeUse') == 'standardVolume' and obj.get('label')
-                and obj.get('volumeRef')):
+                and obj.get('volumeRef')
+                and obj.get('volumeGroupRef') in
+                self._objects['disk_pool_refs']):
             self._objects['volumes']['label_ref'][obj['label']]\
                 = obj['volumeRef']
             self._objects['volumes']['ref_vol'][obj['volumeRef']] = obj
@@ -312,19 +317,26 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
 
     def _get_volume(self, uid):
         label = utils.convert_uuid_to_es_fmt(uid)
+        return self._get_volume_with_label_wwn(label)
+
+    def _get_volume_with_label_wwn(self, label=None, wwn=None):
+        """Searches volume with label or wwn or both."""
+        if not (label or wwn):
+            raise exception.InvalidInput(_('Either volume label or wwn'
+                                           ' is required as input.'))
         try:
             return self._get_cached_volume(label)
         except KeyError:
-            return self._get_latest_volume(uid)
-
-    def _get_latest_volume(self, uid):
-        label = utils.convert_uuid_to_es_fmt(uid)
-        for vol in self._client.list_volumes():
-            if vol.get('label') == label:
+            wwn = wwn.replace(':', '').upper() if wwn else None
+            for vol in self._client.list_volumes():
+                if label and vol.get('label') != label:
+                    continue
+                if wwn and vol.get(self.WORLDWIDENAME).upper() != wwn:
+                    continue
                 self._cache_volume(vol)
-                return self._get_cached_volume(label)
-        raise exception.NetAppDriverException(_("Volume %(uid)s not found.")
-                                              % {'uid': uid})
+                label = vol.get('label')
+                break
+            return self._get_cached_volume(label)
 
     def _get_cached_volume(self, label):
         vol_id = self._objects['volumes']['label_ref'][label]
@@ -573,7 +585,7 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
     def initialize_connection(self, volume, connector):
         """Allow connection to connector and return connection info."""
         initiator_name = connector['initiator']
-        vol = self._get_latest_volume(volume['name_id'])
+        vol = self._get_volume(volume['name_id'])
         iscsi_details = self._get_iscsi_service_details()
         iscsi_portal = self._get_iscsi_portal_for_vol(vol, iscsi_details)
         mapping = self._map_volume_to_host(vol, initiator_name)
@@ -895,3 +907,53 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver):
                                   label)
         finally:
             na_utils.set_safe_attr(self, 'clean_job_running', False)
+
+    @cinder_utils.synchronized('manage_existing')
+    def manage_existing(self, volume, existing_ref):
+        """Brings an existing storage object under Cinder management."""
+        vol = self._get_existing_vol_with_manage_ref(volume, existing_ref)
+        label = utils.convert_uuid_to_es_fmt(volume['id'])
+        if label == vol['label']:
+            LOG.info(_LI("Volume with given ref %s need not be renamed during"
+                         " manage operation."), existing_ref)
+            managed_vol = vol
+        else:
+            managed_vol = self._client.update_volume(vol['id'], label)
+            self._del_volume_frm_cache(vol['label'])
+        self._cache_volume(managed_vol)
+        LOG.info(_LI("Manage operation completed for volume with new label"
+                     " %(label)s and wwn %(wwn)s."),
+                 {'label': label, 'wwn': managed_vol[self.WORLDWIDENAME]})
+
+    def manage_existing_get_size(self, volume, existing_ref):
+        """Return size of volume to be managed by manage_existing.
+
+        When calculating the size, round up to the next GB.
+        """
+        vol = self._get_existing_vol_with_manage_ref(volume, existing_ref)
+        return int(math.ceil(float(vol['capacity']) / units.Gi))
+
+    def _get_existing_vol_with_manage_ref(self, volume, existing_ref):
+        try:
+            return self._get_volume_with_label_wwn(
+                existing_ref.get('source-name'), existing_ref.get('source-id'))
+        except exception.InvalidInput:
+            reason = _('Reference must contain either source-name'
+                       ' or source-id element.')
+            raise exception.ManageExistingInvalidReference(
+                existing_ref=existing_ref, reason=reason)
+        except KeyError:
+            raise exception.ManageExistingInvalidReference(
+                existing_ref=existing_ref,
+                reason=_('Volume not found on configured storage pools.'))
+
+    def unmanage(self, volume):
+        """Removes the specified volume from Cinder management.
+
+           Does not delete the underlying backend storage object. Logs a
+           message to indicate the volume is no longer under Cinder's control.
+        """
+        managed_vol = self._get_volume(volume['id'])
+        LOG.info(_LI("Unmanaged volume with current label %(label)s and wwn "
+                     "%(wwn)s."), {'label': managed_vol['label'],
+                                   'wwn': managed_vol[self.WORLDWIDENAME]})