From 919708fb790b32cc0ab08b27704e685c6c7353c5 Mon Sep 17 00:00:00 2001 From: Navneet Singh Date: Fri, 29 Aug 2014 00:09:17 +0530 Subject: [PATCH] NetApp eseries implementation for manage/unmanage 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 | 116 +++++++++++++++++- cinder/volume/drivers/netapp/eseries/iscsi.py | 88 +++++++++++-- 2 files changed, 189 insertions(+), 15 deletions(-) diff --git a/cinder/tests/test_netapp_eseries_iscsi.py b/cinder/tests/test_netapp_eseries_iscsi.py index 2eab6c9a055..a52c5f6aa4b 100644 --- a/cinder/tests/test_netapp_eseries_iscsi.py +++ b/cinder/tests/test_netapp_eseries_iscsi.py @@ -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) diff --git a/cinder/volume/drivers/netapp/eseries/iscsi.py b/cinder/volume/drivers/netapp/eseries/iscsi.py index 9ba6dadc855..5e5712229b0 100644 --- a/cinder/volume/drivers/netapp/eseries/iscsi.py +++ b/cinder/volume/drivers/netapp/eseries/iscsi.py @@ -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]})