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
This commit is contained in:
Navneet Singh 2014-08-29 00:09:17 +05:30 committed by Bob Callaway
parent 002182ece8
commit 919708fb79
2 changed files with 189 additions and 15 deletions

View 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)

View 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]})