diff --git a/cinder/tests/test_netapp.py b/cinder/tests/test_netapp.py index ebd1e4491e3..26fc57ced8f 100644 --- a/cinder/tests/test_netapp.py +++ b/cinder/tests/test_netapp.py @@ -1,4 +1,3 @@ - # Copyright (c) 2012 NetApp, Inc. # All Rights Reserved. # @@ -26,20 +25,18 @@ import mock import six from cinder import exception -from cinder.i18n import _ from cinder.openstack.common import log as logging from cinder import test from cinder.volume import configuration as conf -from cinder.volume.drivers.netapp.api import NaElement -from cinder.volume.drivers.netapp.api import NaServer from cinder.volume.drivers.netapp import common +from cinder.volume.drivers.netapp.dataontap.client import client_base +from cinder.volume.drivers.netapp.dataontap import ssc_cmode from cinder.volume.drivers.netapp.options import netapp_7mode_opts from cinder.volume.drivers.netapp.options import netapp_basicauth_opts from cinder.volume.drivers.netapp.options import netapp_cluster_opts from cinder.volume.drivers.netapp.options import netapp_connection_opts from cinder.volume.drivers.netapp.options import netapp_provisioning_opts from cinder.volume.drivers.netapp.options import netapp_transport_opts -from cinder.volume.drivers.netapp import ssc_utils LOG = logging.getLogger("cinder.volume.driver") @@ -525,7 +522,7 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase): 'id': 'lun1', 'provider_auth': None, 'project_id': 'project', 'display_name': None, 'display_description': 'lun1', 'volume_type_id': None, 'host': 'hostname@backend#vol1'} - vol1 = ssc_utils.NetAppVolume('lun1', 'openstack') + vol1 = ssc_cmode.NetAppVolume('lun1', 'openstack') vol1.state['vserver_root'] = False vol1.state['status'] = 'online' vol1.state['junction_active'] = True @@ -553,15 +550,13 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase): def _custom_setup(self): self.stubs.Set( - ssc_utils, 'refresh_cluster_ssc', + ssc_cmode, 'refresh_cluster_ssc', lambda a, b, c, synchronous: None) configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) self.stubs.Set(httplib, 'HTTPConnection', FakeDirectCmodeHTTPConnection) driver.do_setup(context='') - client = driver.client - client.set_api_version(1, 15) self.driver = driver self.driver.ssc_vols = self.ssc_map @@ -576,52 +571,65 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase): return configuration def test_connect(self): + self.driver.library.zapi_client = mock.MagicMock() + self.driver.library.zapi_client.get_ontapi_version.return_value = \ + (1, 20) self.driver.check_for_setup_error() def test_do_setup_all_default(self): configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('80', driver.client.get_port()) - self.assertEqual('http', driver.client.get_transport_type()) + na_server = driver.library.zapi_client.get_connection() + self.assertEqual('80', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) def test_do_setup_http_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'http' driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('80', driver.client.get_port()) - self.assertEqual('http', driver.client.get_transport_type()) + na_server = driver.library.zapi_client.get_connection() + self.assertEqual('80', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) def test_do_setup_https_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'https' driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() + driver.library._get_root_volume_name = mock.Mock() driver.do_setup(context='') - self.assertEqual('443', driver.client.get_port()) - self.assertEqual('https', driver.client.get_transport_type()) + na_server = driver.library.zapi_client.get_connection() + self.assertEqual('443', na_server.get_port()) + self.assertEqual('https', na_server.get_transport_type()) + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) def test_do_setup_http_non_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_server_port = 81 driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('81', driver.client.get_port()) - self.assertEqual('http', driver.client.get_transport_type()) + na_server = driver.library.zapi_client.get_connection() + self.assertEqual('81', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) def test_do_setup_https_non_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'https' configuration.netapp_server_port = 446 driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() + driver.library._get_root_volume_name = mock.Mock() driver.do_setup(context='') - self.assertEqual('446', driver.client.get_port()) - self.assertEqual('https', driver.client.get_transport_type()) + na_server = driver.library.zapi_client.get_connection() + self.assertEqual('446', na_server.get_port()) + self.assertEqual('https', na_server.get_transport_type()) def test_create_destroy(self): self.driver.create_volume(self.volume) @@ -669,10 +677,8 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase): raise AssertionError('Target portal is none') def test_vol_stats(self): - self.driver.get_volume_stats(refresh=True) - stats = self.driver._stats + stats = self.driver.get_volume_stats(refresh=True) self.assertEqual(stats['vendor_name'], 'NetApp') - self.assertTrue(stats['pools'][0]['pool_name']) def test_create_vol_snapshot_diff_size_resize(self): self.driver.create_volume(self.volume) @@ -1174,10 +1180,8 @@ class NetAppDirect7modeISCSIDriverTestCase_NV( self.stubs.Set(httplib, 'HTTPConnection', FakeDirect7modeHTTPConnection) driver.do_setup(context='') - client = driver.client - client.set_api_version(1, 9) + driver.root_volume_name = 'root' self.driver = driver - self.driver.root_volume_name = 'root' def _set_config(self, configuration): configuration.netapp_storage_family = 'ontap_7mode' @@ -1195,15 +1199,22 @@ class NetAppDirect7modeISCSIDriverTestCase_NV( self.driver.delete_volume(self.volume) self.driver.volume_list = [] + def test_connect(self): + self.driver.driver.library.zapi_client = mock.MagicMock() + self.driver.driver.library.zapi_client.get_ontapi_version.\ + return_value = (1, 20) + self.driver.check_for_setup_error() + def test_check_for_setup_error_version(self): drv = self.driver - delattr(drv.client, '_api_version') + drv.zapi_client = mock.Mock() + drv.zapi_client.get_ontapi_version.return_value = None # check exception raises when version not found self.assertRaises(exception.VolumeBackendAPIException, drv.check_for_setup_error) - drv.client.set_api_version(1, 8) + drv.zapi_client.get_ontapi_version.return_value = (1, 8) # check exception raises when not supported version self.assertRaises(exception.VolumeBackendAPIException, @@ -1224,8 +1235,6 @@ class NetAppDirect7modeISCSIDriverTestCase_WV( self.stubs.Set(httplib, 'HTTPConnection', FakeDirect7modeHTTPConnection) driver.do_setup(context='') - client = driver.client - client.set_api_version(1, 9) self.driver = driver self.driver.root_volume_name = 'root' @@ -1239,132 +1248,3 @@ class NetAppDirect7modeISCSIDriverTestCase_WV( configuration.netapp_server_port = None configuration.netapp_vfiler = 'openstack' return configuration - - -class NetAppApiElementTransTests(test.TestCase): - """Test case for NetApp api element translations.""" - - def setUp(self): - super(NetAppApiElementTransTests, self).setUp() - - def test_translate_struct_dict_unique_key(self): - """Tests if dict gets properly converted to NaElements.""" - root = NaElement('root') - child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'} - root.translate_struct(child) - self.assertEqual(len(root.get_children()), 3) - self.assertEqual(root.get_child_content('e1'), 'v1') - self.assertEqual(root.get_child_content('e2'), 'v2') - self.assertEqual(root.get_child_content('e3'), 'v3') - - def test_translate_struct_dict_nonunique_key(self): - """Tests if list/dict gets properly converted to NaElements.""" - root = NaElement('root') - child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}] - root.translate_struct(child) - self.assertEqual(len(root.get_children()), 3) - children = root.get_children() - for c in children: - if c.get_name() == 'e1': - self.assertIn(c.get_content(), ['v1', 'v3']) - else: - self.assertEqual(c.get_content(), 'v2') - - def test_translate_struct_list(self): - """Tests if list gets properly converted to NaElements.""" - root = NaElement('root') - child = ['e1', 'e2'] - root.translate_struct(child) - self.assertEqual(len(root.get_children()), 2) - self.assertIsNone(root.get_child_content('e1')) - self.assertIsNone(root.get_child_content('e2')) - - def test_translate_struct_tuple(self): - """Tests if tuple gets properly converted to NaElements.""" - root = NaElement('root') - child = ('e1', 'e2') - root.translate_struct(child) - self.assertEqual(len(root.get_children()), 2) - self.assertIsNone(root.get_child_content('e1')) - self.assertIsNone(root.get_child_content('e2')) - - def test_translate_invalid_struct(self): - """Tests if invalid data structure raises exception.""" - root = NaElement('root') - child = 'random child element' - self.assertRaises(ValueError, root.translate_struct, child) - - def test_setter_builtin_types(self): - """Tests str, int, float get converted to NaElement.""" - root = NaElement('root') - root['e1'] = 'v1' - root['e2'] = 1 - root['e3'] = 2.0 - root['e4'] = 8l - self.assertEqual(len(root.get_children()), 4) - self.assertEqual(root.get_child_content('e1'), 'v1') - self.assertEqual(root.get_child_content('e2'), '1') - self.assertEqual(root.get_child_content('e3'), '2.0') - self.assertEqual(root.get_child_content('e4'), '8') - - def test_setter_na_element(self): - """Tests na_element gets appended as child.""" - root = NaElement('root') - root['e1'] = NaElement('nested') - self.assertEqual(len(root.get_children()), 1) - e1 = root.get_child_by_name('e1') - self.assertIsInstance(e1, NaElement) - self.assertIsInstance(e1.get_child_by_name('nested'), NaElement) - - def test_setter_child_dict(self): - """Tests dict is appended as child to root.""" - root = NaElement('root') - root['d'] = {'e1': 'v1', 'e2': 'v2'} - e1 = root.get_child_by_name('d') - self.assertIsInstance(e1, NaElement) - sub_ch = e1.get_children() - self.assertEqual(len(sub_ch), 2) - for c in sub_ch: - self.assertIn(c.get_name(), ['e1', 'e2']) - if c.get_name() == 'e1': - self.assertEqual(c.get_content(), 'v1') - else: - self.assertEqual(c.get_content(), 'v2') - - def test_setter_child_list_tuple(self): - """Tests list/tuple are appended as child to root.""" - root = NaElement('root') - root['l'] = ['l1', 'l2'] - root['t'] = ('t1', 't2') - l = root.get_child_by_name('l') - self.assertIsInstance(l, NaElement) - t = root.get_child_by_name('t') - self.assertIsInstance(t, NaElement) - for le in l.get_children(): - self.assertIn(le.get_name(), ['l1', 'l2']) - for te in t.get_children(): - self.assertIn(te.get_name(), ['t1', 't2']) - - def test_setter_no_value(self): - """Tests key with None value.""" - root = NaElement('root') - root['k'] = None - self.assertIsNone(root.get_child_content('k')) - - def test_setter_invalid_value(self): - """Tests invalid value raises exception.""" - root = NaElement('root') - try: - root['k'] = NaServer('localhost') - except Exception as e: - if not isinstance(e, TypeError): - self.fail(_('Error not a TypeError.')) - - def test_setter_invalid_key(self): - """Tests invalid value raises exception.""" - root = NaElement('root') - try: - root[None] = 'value' - except Exception as e: - if not isinstance(e, KeyError): - self.fail(_('Error not a KeyError.')) diff --git a/cinder/tests/test_netapp_eseries_iscsi.py b/cinder/tests/test_netapp_eseries_iscsi.py index 6c6cf5d06b0..4aae0d14c2a 100644 --- a/cinder/tests/test_netapp_eseries_iscsi.py +++ b/cinder/tests/test_netapp_eseries_iscsi.py @@ -32,9 +32,9 @@ from cinder.volume.drivers.netapp import common from cinder.volume.drivers.netapp.eseries import client from cinder.volume.drivers.netapp.eseries import iscsi from cinder.volume.drivers.netapp.eseries.iscsi import LOG as driver_log +from cinder.volume.drivers.netapp.eseries import utils from cinder.volume.drivers.netapp.options import netapp_basicauth_opts from cinder.volume.drivers.netapp.options import netapp_eseries_opts -import cinder.volume.drivers.netapp.utils as na_utils LOG = logging.getLogger(__name__) @@ -590,7 +590,7 @@ class FakeEseriesHTTPSession(object): raise exception.Invalid() -class NetAppEseriesIscsiDriverTestCase(test.TestCase): +class NetAppEseriesISCSIDriverTestCase(test.TestCase): """Test case for NetApp e-series iscsi driver.""" volume = {'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1, @@ -629,13 +629,13 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase): 'project_id': 'project', 'display_name': None, 'display_description': 'lun1', 'volume_type_id': None} - fake_eseries_volume_label = na_utils.convert_uuid_to_es_fmt(volume['id']) + fake_eseries_volume_label = utils.convert_uuid_to_es_fmt(volume['id']) connector = {'initiator': 'iqn.1998-01.com.vmware:localhost-28a58148'} fake_size_gb = volume['size'] fake_eseries_pool_label = 'DDP' def setUp(self): - super(NetAppEseriesIscsiDriverTestCase, self).setUp() + super(NetAppEseriesISCSIDriverTestCase, self).setUp() self._custom_setup() def _custom_setup(self): @@ -781,7 +781,7 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase): self.driver.delete_snapshot(self.snapshot) self.driver.delete_volume(self.volume) - @mock.patch.object(iscsi.Driver, '_get_volume', + @mock.patch.object(iscsi.NetAppEseriesISCSIDriver, '_get_volume', mock.Mock(return_value={'volumeGroupRef': 'fake_ref'})) def test_get_pool(self): self.driver._objects['pools'] = [{'volumeGroupRef': 'fake_ref', @@ -789,14 +789,14 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase): pool = self.driver.get_pool({'id': 'fake-uuid'}) self.assertEqual(pool, 'ddp1') - @mock.patch.object(iscsi.Driver, '_get_volume', + @mock.patch.object(iscsi.NetAppEseriesISCSIDriver, '_get_volume', mock.Mock(return_value={'volumeGroupRef': 'fake_ref'})) def test_get_pool_no_pools(self): self.driver._objects['pools'] = [] pool = self.driver.get_pool({'id': 'fake-uuid'}) self.assertEqual(pool, None) - @mock.patch.object(iscsi.Driver, '_get_volume', + @mock.patch.object(iscsi.NetAppEseriesISCSIDriver, '_get_volume', mock.Mock(return_value={'volumeGroupRef': 'fake_ref'})) def test_get_pool_no_match(self): self.driver._objects['pools'] = [{'volumeGroupRef': 'fake_ref2', @@ -804,7 +804,8 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase): pool = self.driver.get_pool({'id': 'fake-uuid'}) self.assertEqual(pool, None) - @mock.patch.object(iscsi.Driver, '_create_volume', mock.Mock()) + @mock.patch.object(iscsi.NetAppEseriesISCSIDriver, '_create_volume', + mock.Mock()) def test_create_volume(self): self.driver.create_volume(self.volume) self.driver._create_volume.assert_called_with( diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py index 4f75bf5ef68..9c75e5b8ed8 100644 --- a/cinder/tests/test_netapp_nfs.py +++ b/cinder/tests/test_netapp_nfs.py @@ -20,19 +20,25 @@ from lxml import etree import mock import mox from mox import IgnoreArg -from mox import IsA import six -from cinder import context from cinder import exception from cinder.i18n import _LW from cinder.image import image_utils from cinder.openstack.common import log as logging from cinder import test from cinder.volume import configuration as conf -from cinder.volume.drivers.netapp import api from cinder.volume.drivers.netapp import common -from cinder.volume.drivers.netapp import nfs as netapp_nfs +from cinder.volume.drivers.netapp.dataontap.client import api +from cinder.volume.drivers.netapp.dataontap.client import client_7mode +from cinder.volume.drivers.netapp.dataontap.client import client_base +from cinder.volume.drivers.netapp.dataontap.client import client_cmode +from cinder.volume.drivers.netapp.dataontap import nfs_7mode \ + as netapp_nfs_7mode +from cinder.volume.drivers.netapp.dataontap import nfs_base +from cinder.volume.drivers.netapp.dataontap import nfs_cmode \ + as netapp_nfs_cmode +from cinder.volume.drivers.netapp.dataontap import ssc_cmode from cinder.volume.drivers.netapp import utils @@ -42,11 +48,24 @@ CONF = cfg.CONF LOG = logging.getLogger(__name__) +CONNECTION_INFO = {'hostname': 'fake_host', + 'transport_type': 'https', + 'port': 443, + 'username': 'admin', + 'password': 'passw0rd'} +FAKE_VSERVER = 'fake_vserver' + + def create_configuration(): configuration = mox.MockObject(conf.Configuration) configuration.append_config_values(mox.IgnoreArg()) configuration.nfs_mount_point_base = '/mnt/test' configuration.nfs_mount_options = None + configuration.netapp_server_hostname = CONNECTION_INFO['hostname'] + configuration.netapp_transport_type = CONNECTION_INFO['transport_type'] + configuration.netapp_server_port = CONNECTION_INFO['port'] + configuration.netapp_login = CONNECTION_INFO['username'] + configuration.netapp_password = CONNECTION_INFO['password'] return configuration @@ -89,12 +108,22 @@ class FakeResponse(object): self.Reason = 'Sample error' -class NetappDirectCmodeNfsDriverTestCase(test.TestCase): +class NetAppCmodeNfsDriverTestCase(test.TestCase): """Test direct NetApp C Mode driver.""" def setUp(self): - super(NetappDirectCmodeNfsDriverTestCase, self).setUp() + super(NetAppCmodeNfsDriverTestCase, self).setUp() self._custom_setup() + def _custom_setup(self): + kwargs = {} + kwargs['netapp_mode'] = 'proxy' + kwargs['configuration'] = create_configuration() + self._driver = netapp_nfs_cmode.NetAppCmodeNfsDriver(**kwargs) + self._driver.zapi_client = mock.Mock() + + config = self._driver.configuration + config.netapp_vserver = FAKE_VSERVER + def test_create_snapshot(self): """Test snapshot can be created and deleted.""" mox = self.mox @@ -179,62 +208,24 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase): mox.VerifyAll() - def _custom_setup(self): - kwargs = {} - kwargs['netapp_mode'] = 'proxy' - kwargs['configuration'] = create_configuration() - self._driver = netapp_nfs.NetAppDirectCmodeNfsDriver(**kwargs) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup') + @mock.patch.object(client_cmode.Client, '__init__', return_value=None) + def test_do_setup(self, mock_client_init, mock_super_do_setup): + context = mock.Mock() + self._driver.do_setup(context) + mock_client_init.assert_called_once_with(vserver=FAKE_VSERVER, + **CONNECTION_INFO) + mock_super_do_setup.assert_called_once_with(context) - def test_check_for_setup_error(self): - mox = self.mox - drv = self._driver - required_flags = [ - 'netapp_login', - 'netapp_password', - 'netapp_server_hostname'] - - # set required flags - for flag in required_flags: - setattr(drv.configuration, flag, None) - # check exception raises when flags are not set - self.assertRaises(exception.CinderException, - drv.check_for_setup_error) - - # set required flags - for flag in required_flags: - setattr(drv.configuration, flag, 'val') - setattr(drv, 'ssc_enabled', False) - - mox.StubOutWithMock(netapp_nfs.NetAppDirectNfsDriver, '_check_flags') - - netapp_nfs.NetAppDirectNfsDriver._check_flags() - mox.ReplayAll() - - drv.check_for_setup_error() - - mox.VerifyAll() - - # restore initial FLAGS - for flag in required_flags: - delattr(drv.configuration, flag) - - def test_do_setup(self): - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, 'do_setup') - mox.StubOutWithMock(drv, '_get_client') - mox.StubOutWithMock(drv, '_do_custom_setup') - - netapp_nfs.NetAppNFSDriver.do_setup(IgnoreArg()) - drv._get_client() - drv._do_custom_setup(IgnoreArg()) - - mox.ReplayAll() - - drv.do_setup(IsA(context.RequestContext)) - - mox.VerifyAll() + @mock.patch.object(nfs_base.NetAppNfsDriver, 'check_for_setup_error') + @mock.patch.object(ssc_cmode, 'check_ssc_api_permissions') + def test_check_for_setup_error(self, mock_ssc_api_permission_check, + mock_super_check_for_setup_error): + self._driver.zapi_client = mock.Mock() + self._driver.check_for_setup_error() + mock_ssc_api_permission_check.assert_called_once_with( + self._driver.zapi_client) + mock_super_check_for_setup_error.assert_called_once_with() def _prepare_clone_mock(self, status): drv = self._driver @@ -816,74 +807,85 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase): configuration.nfs_shares_config = '/nfs' return configuration - @mock.patch.object(netapp_nfs.NetAppNFSDriver, 'do_setup') - def test_do_setup_all_default(self, mock_set_up): + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) + def test_do_setup_all_default(self): configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('80', driver._client.get_port()) - self.assertEqual('http', driver._client.get_transport_type()) + na_server = driver.zapi_client.get_connection() + self.assertEqual('80', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) - @mock.patch.object(netapp_nfs.NetAppNFSDriver, 'do_setup') - def test_do_setup_http_default_port(self, mock_setup): + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) + def test_do_setup_http_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'http' driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('80', driver._client.get_port()) - self.assertEqual('http', driver._client.get_transport_type()) + na_server = driver.zapi_client.get_connection() + self.assertEqual('80', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) - @mock.patch.object(netapp_nfs.NetAppNFSDriver, 'do_setup') - def test_do_setup_https_default_port(self, mock_setup): + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) + def test_do_setup_https_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'https' driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('443', driver._client.get_port()) - self.assertEqual('https', driver._client.get_transport_type()) + na_server = driver.zapi_client.get_connection() + self.assertEqual('443', na_server.get_port()) + self.assertEqual('https', na_server.get_transport_type()) - @mock.patch.object(netapp_nfs.NetAppNFSDriver, 'do_setup') - def test_do_setup_http_non_default_port(self, mock_setup): + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) + def test_do_setup_http_non_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_server_port = 81 driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('81', driver._client.get_port()) - self.assertEqual('http', driver._client.get_transport_type()) + na_server = driver.zapi_client.get_connection() + self.assertEqual('81', na_server.get_port()) + self.assertEqual('http', na_server.get_transport_type()) - @mock.patch.object(netapp_nfs.NetAppNFSDriver, 'do_setup') - def test_do_setup_https_non_default_port(self, mock_setup): + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) + def test_do_setup_https_non_default_port(self): configuration = self._set_config(create_configuration()) configuration.netapp_transport_type = 'https' configuration.netapp_server_port = 446 driver = common.NetAppDriver(configuration=configuration) - driver._do_custom_setup = mock.Mock() driver.do_setup(context='') - self.assertEqual('446', driver._client.get_port()) - self.assertEqual('https', driver._client.get_transport_type()) + na_server = driver.zapi_client.get_connection() + self.assertEqual('446', na_server.get_port()) + self.assertEqual('https', na_server.get_transport_type()) -class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): +class NetAppCmodeNfsDriverOnlyTestCase(test.TestCase): """Test direct NetApp C Mode driver only and not inherit.""" def setUp(self): - super(NetappDirectCmodeNfsDriverOnlyTestCase, self).setUp() + super(NetAppCmodeNfsDriverOnlyTestCase, self).setUp() self._custom_setup() def _custom_setup(self): kwargs = {} kwargs['netapp_mode'] = 'proxy' kwargs['configuration'] = create_configuration() - self._driver = netapp_nfs.NetAppDirectCmodeNfsDriver(**kwargs) + self._driver = netapp_nfs_cmode.NetAppCmodeNfsDriver(**kwargs) self._driver.ssc_enabled = True self._driver.configuration.netapp_copyoffload_tool_path = 'cof_path' + self._driver.zapi_client = mock.Mock() + @mock.patch.object(netapp_nfs_cmode, 'get_volume_extra_specs') @mock.patch.object(utils, 'LOG', mock.Mock()) - @mock.patch.object(netapp_nfs, 'get_volume_extra_specs') def test_create_volume(self, mock_volume_extra_specs): drv = self._driver drv.ssc_enabled = False @@ -899,11 +901,14 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): self.assertEqual(0, utils.LOG.warning.call_count) @mock.patch.object(utils, 'LOG', mock.Mock()) - @mock.patch.object(netapp_nfs, 'get_volume_extra_specs') - def test_create_volume_obsolete_extra_spec(self, mock_volume_extra_specs): + def test_create_volume_obsolete_extra_spec(self): drv = self._driver drv.ssc_enabled = False extra_specs = {'netapp:raid_type': 'raid4'} + mock_volume_extra_specs = mock.Mock() + self.mock_object(netapp_nfs_cmode, + 'get_volume_extra_specs', + mock_volume_extra_specs) mock_volume_extra_specs.return_value = extra_specs fake_share = 'localhost:myshare' host = 'hostname@backend#' + fake_share @@ -915,15 +920,17 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): utils.LOG.warning.assert_called_once_with(warn_msg) @mock.patch.object(utils, 'LOG', mock.Mock()) - @mock.patch.object(netapp_nfs, 'get_volume_extra_specs') - def test_create_volume_deprecated_extra_spec(self, - mock_volume_extra_specs): + def test_create_volume_deprecated_extra_spec(self): drv = self._driver drv.ssc_enabled = False extra_specs = {'netapp_thick_provisioned': 'true'} - mock_volume_extra_specs.return_value = extra_specs fake_share = 'localhost:myshare' host = 'hostname@backend#' + fake_share + mock_volume_extra_specs = mock.Mock() + self.mock_object(netapp_nfs_cmode, + 'get_volume_extra_specs', + mock_volume_extra_specs) + mock_volume_extra_specs.return_value = extra_specs with mock.patch.object(drv, '_ensure_shares_mounted'): with mock.patch.object(drv, '_do_create_volume'): self._driver.create_volume(FakeVolume(host, 1)) @@ -939,7 +946,7 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): self.assertRaises(exception.InvalidHost, self._driver.create_volume, FakeVolume(host, 1)) - @mock.patch.object(netapp_nfs, 'get_volume_extra_specs') + @mock.patch.object(netapp_nfs_cmode, 'get_volume_extra_specs') def test_create_volume_with_qos_policy(self, mock_volume_extra_specs): drv = self._driver drv.ssc_enabled = False @@ -968,8 +975,7 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): volume = {'id': 'vol_id', 'name': 'name'} image_service = object() image_id = 'image_id' - drv._client = mock.Mock() - drv._client.get_api_version = mock.Mock(return_value=(1, 20)) + drv.zapi_client.get_ontapi_version = mock.Mock(return_value=(1, 20)) drv._try_copyoffload = mock.Mock() drv._get_provider_location = mock.Mock(return_value='share') drv._get_vol_for_share = mock.Mock(return_value='vol') @@ -987,10 +993,9 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): volume = {'id': 'vol_id', 'name': 'name'} image_service = object() image_id = 'image_id' - drv._client = mock.Mock() - drv._client.get_api_version = mock.Mock(return_value=(1, 20)) + drv.zapi_client.get_ontapi_version = mock.Mock(return_value=(1, 20)) drv._try_copyoffload = mock.Mock(side_effect=Exception()) - netapp_nfs.NetAppNFSDriver.copy_image_to_volume = mock.Mock() + nfs_base.NetAppNfsDriver.copy_image_to_volume = mock.Mock() drv._get_provider_location = mock.Mock(return_value='share') drv._get_vol_for_share = mock.Mock(return_value='vol') drv._update_stale_vols = mock.Mock() @@ -999,7 +1004,7 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): drv._try_copyoffload.assert_called_once_with(context, volume, image_service, image_id) - netapp_nfs.NetAppNFSDriver.copy_image_to_volume.\ + nfs_base.NetAppNfsDriver.copy_image_to_volume.\ assert_called_once_with(context, volume, image_service, image_id) drv._update_stale_vols.assert_called_once_with('vol') @@ -1170,11 +1175,13 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase): drv._post_clone_image.assert_called_with(volume) -class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): +class NetApp7modeNfsDriverTestCase(NetAppCmodeNfsDriverTestCase): """Test direct NetApp C Mode driver.""" + def _custom_setup(self): - self._driver = netapp_nfs.NetAppDirect7modeNfsDriver( + self._driver = netapp_nfs_7mode.NetApp7modeNfsDriver( configuration=create_configuration()) + self._driver.zapi_client = mock.Mock() def _prepare_delete_snapshot_mock(self, snapshot_exists): drv = self._driver @@ -1207,68 +1214,29 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): self.assertRaises(exception.InvalidHost, self._driver.create_volume, FakeVolume(host, 1)) - def test_check_for_setup_error_version(self): - drv = self._driver - drv._client = api.NaServer("127.0.0.1") + @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup') + @mock.patch.object(client_7mode.Client, '__init__', return_value=None) + def test_do_setup(self, mock_client_init, mock_super_do_setup): + context = mock.Mock() + self._driver.do_setup(context) + mock_client_init.assert_called_once_with(**CONNECTION_INFO) + mock_super_do_setup.assert_called_once_with(context) - # check exception raises when version not found + @mock.patch.object(nfs_base.NetAppNfsDriver, 'check_for_setup_error') + def test_check_for_setup_error(self, mock_super_check_for_setup_error): + self._driver.zapi_client.get_ontapi_version.return_value = (1, 20) + self.assertIsNone(self._driver.check_for_setup_error()) + mock_super_check_for_setup_error.assert_called_once_with() + + def test_check_for_setup_error_old_version(self): + self._driver.zapi_client.get_ontapi_version.return_value = (1, 8) self.assertRaises(exception.VolumeBackendAPIException, - drv.check_for_setup_error) + self._driver.check_for_setup_error) - drv._client.set_api_version(1, 8) - - # check exception raises when not supported version + def test_check_for_setup_error_no_version(self): + self._driver.zapi_client.get_ontapi_version.return_value = None self.assertRaises(exception.VolumeBackendAPIException, - drv.check_for_setup_error) - - def test_check_for_setup_error(self): - mox = self.mox - drv = self._driver - drv._client = api.NaServer("127.0.0.1") - drv._client.set_api_version(1, 9) - required_flags = [ - 'netapp_transport_type', - 'netapp_login', - 'netapp_password', - 'netapp_server_hostname', - 'netapp_server_port'] - - # set required flags - for flag in required_flags: - setattr(drv.configuration, flag, None) - # check exception raises when flags are not set - self.assertRaises(exception.CinderException, - drv.check_for_setup_error) - - # set required flags - for flag in required_flags: - setattr(drv.configuration, flag, 'val') - - mox.ReplayAll() - - drv.check_for_setup_error() - - mox.VerifyAll() - - # restore initial FLAGS - for flag in required_flags: - delattr(drv.configuration, flag) - - def test_do_setup(self): - mox = self.mox - drv = self._driver - mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, 'do_setup') - mox.StubOutWithMock(drv, '_get_client') - mox.StubOutWithMock(drv, '_do_custom_setup') - netapp_nfs.NetAppNFSDriver.do_setup(IgnoreArg()) - drv._get_client() - drv._do_custom_setup(IgnoreArg()) - - mox.ReplayAll() - - drv.do_setup(IsA(context.RequestContext)) - - mox.VerifyAll() + self._driver.check_for_setup_error) def _prepare_clone_mock(self, status): drv = self._driver @@ -1311,7 +1279,7 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): self.assertEqual(pool, 'fake-share') def _set_config(self, configuration): - super(NetappDirect7modeNfsDriverTestCase, self)._set_config( + super(NetApp7modeNfsDriverTestCase, self)._set_config( configuration) configuration.netapp_storage_family = 'ontap_7mode' return configuration diff --git a/cinder/tests/test_netapp_ssc.py b/cinder/tests/test_netapp_ssc.py index 11ea2289207..8e78f5d5543 100644 --- a/cinder/tests/test_netapp_ssc.py +++ b/cinder/tests/test_netapp_ssc.py @@ -1,4 +1,3 @@ - # Copyright (c) 2012 NetApp, Inc. # All Rights Reserved. # @@ -25,8 +24,8 @@ import six from cinder import exception from cinder import test -from cinder.volume.drivers.netapp import api -from cinder.volume.drivers.netapp import ssc_utils +from cinder.volume.drivers.netapp.dataontap.client import api +from cinder.volume.drivers.netapp.dataontap import ssc_cmode class FakeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): @@ -293,7 +292,7 @@ class FakeDirectCmodeHTTPConnection(object): def createNetAppVolume(**kwargs): - vol = ssc_utils.NetAppVolume(kwargs['name'], kwargs['vs']) + vol = ssc_cmode.NetAppVolume(kwargs['name'], kwargs['vs']) vol.state['vserver_root'] = kwargs.get('vs_root') vol.state['status'] = kwargs.get('status') vol.state['junction_active'] = kwargs.get('junc_active') @@ -384,29 +383,29 @@ class SscUtilsTestCase(test.TestCase): 'rel_type': 'data_protection', 'mirr_state': 'broken'}]} - self.mox.StubOutWithMock(ssc_utils, 'query_cluster_vols_for_ssc') - self.mox.StubOutWithMock(ssc_utils, 'get_sis_vol_dict') - self.mox.StubOutWithMock(ssc_utils, 'get_snapmirror_vol_dict') - self.mox.StubOutWithMock(ssc_utils, 'query_aggr_options') - self.mox.StubOutWithMock(ssc_utils, 'query_aggr_storage_disk') - ssc_utils.query_cluster_vols_for_ssc( + self.mox.StubOutWithMock(ssc_cmode, 'query_cluster_vols_for_ssc') + self.mox.StubOutWithMock(ssc_cmode, 'get_sis_vol_dict') + self.mox.StubOutWithMock(ssc_cmode, 'get_snapmirror_vol_dict') + self.mox.StubOutWithMock(ssc_cmode, 'query_aggr_options') + self.mox.StubOutWithMock(ssc_cmode, 'query_aggr_storage_disk') + ssc_cmode.query_cluster_vols_for_ssc( na_server, vserver, None).AndReturn(test_vols) - ssc_utils.get_sis_vol_dict(na_server, vserver, None).AndReturn(sis) - ssc_utils.get_snapmirror_vol_dict(na_server, vserver, None).AndReturn( + ssc_cmode.get_sis_vol_dict(na_server, vserver, None).AndReturn(sis) + ssc_cmode.get_snapmirror_vol_dict(na_server, vserver, None).AndReturn( mirrored) raiddp = {'ha_policy': 'cfo', 'raid_type': 'raiddp'} - ssc_utils.query_aggr_options( + ssc_cmode.query_aggr_options( na_server, IgnoreArg()).AndReturn(raiddp) - ssc_utils.query_aggr_storage_disk( + ssc_cmode.query_aggr_storage_disk( na_server, IgnoreArg()).AndReturn('SSD') raid4 = {'ha_policy': 'cfo', 'raid_type': 'raid4'} - ssc_utils.query_aggr_options( + ssc_cmode.query_aggr_options( na_server, IgnoreArg()).AndReturn(raid4) - ssc_utils.query_aggr_storage_disk( + ssc_cmode.query_aggr_storage_disk( na_server, IgnoreArg()).AndReturn('SAS') self.mox.ReplayAll() - res_vols = ssc_utils.get_cluster_vols_with_ssc( + res_vols = ssc_cmode.get_cluster_vols_with_ssc( na_server, vserver, volume=None) self.mox.VerifyAll() @@ -430,24 +429,24 @@ class SscUtilsTestCase(test.TestCase): 'rel_type': 'data_protection', 'mirr_state': 'snapmirrored'}]} - self.mox.StubOutWithMock(ssc_utils, 'query_cluster_vols_for_ssc') - self.mox.StubOutWithMock(ssc_utils, 'get_sis_vol_dict') - self.mox.StubOutWithMock(ssc_utils, 'get_snapmirror_vol_dict') - self.mox.StubOutWithMock(ssc_utils, 'query_aggr_options') - self.mox.StubOutWithMock(ssc_utils, 'query_aggr_storage_disk') - ssc_utils.query_cluster_vols_for_ssc( + self.mox.StubOutWithMock(ssc_cmode, 'query_cluster_vols_for_ssc') + self.mox.StubOutWithMock(ssc_cmode, 'get_sis_vol_dict') + self.mox.StubOutWithMock(ssc_cmode, 'get_snapmirror_vol_dict') + self.mox.StubOutWithMock(ssc_cmode, 'query_aggr_options') + self.mox.StubOutWithMock(ssc_cmode, 'query_aggr_storage_disk') + ssc_cmode.query_cluster_vols_for_ssc( na_server, vserver, 'vola').AndReturn(test_vols) - ssc_utils.get_sis_vol_dict( + ssc_cmode.get_sis_vol_dict( na_server, vserver, 'vola').AndReturn(sis) - ssc_utils.get_snapmirror_vol_dict( + ssc_cmode.get_snapmirror_vol_dict( na_server, vserver, 'vola').AndReturn(mirrored) raiddp = {'ha_policy': 'cfo', 'raid_type': 'raiddp'} - ssc_utils.query_aggr_options( + ssc_cmode.query_aggr_options( na_server, 'aggr1').AndReturn(raiddp) - ssc_utils.query_aggr_storage_disk(na_server, 'aggr1').AndReturn('SSD') + ssc_cmode.query_aggr_storage_disk(na_server, 'aggr1').AndReturn('SSD') self.mox.ReplayAll() - res_vols = ssc_utils.get_cluster_vols_with_ssc( + res_vols = ssc_cmode.get_cluster_vols_with_ssc( na_server, vserver, volume='vola') self.mox.VerifyAll() @@ -460,12 +459,12 @@ class SscUtilsTestCase(test.TestCase): test_vols = set( [self.vol1, self.vol2, self.vol3, self.vol4, self.vol5]) - self.mox.StubOutWithMock(ssc_utils, 'get_cluster_vols_with_ssc') - ssc_utils.get_cluster_vols_with_ssc( + self.mox.StubOutWithMock(ssc_cmode, 'get_cluster_vols_with_ssc') + ssc_cmode.get_cluster_vols_with_ssc( na_server, vserver).AndReturn(test_vols) self.mox.ReplayAll() - res_map = ssc_utils.get_cluster_ssc(na_server, vserver) + res_map = ssc_cmode.get_cluster_ssc(na_server, vserver) self.mox.VerifyAll() self.assertEqual(len(res_map['mirrored']), 1) @@ -491,16 +490,16 @@ class SscUtilsTestCase(test.TestCase): for type in test_map.keys(): # type extra_specs = {test_map[type][0]: 'true'} - res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs) + res = ssc_cmode.get_volumes_for_specs(ssc_map, extra_specs) self.assertEqual(len(res), len(ssc_map[type])) # opposite type extra_specs = {test_map[type][1]: 'true'} - res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs) + res = ssc_cmode.get_volumes_for_specs(ssc_map, extra_specs) self.assertEqual(len(res), len(ssc_map['all'] - ssc_map[type])) # both types extra_specs =\ {test_map[type][0]: 'true', test_map[type][1]: 'true'} - res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs) + res = ssc_cmode.get_volumes_for_specs(ssc_map, extra_specs) self.assertEqual(len(res), len(ssc_map['all'])) def test_vols_for_optional_specs(self): @@ -514,13 +513,13 @@ class SscUtilsTestCase(test.TestCase): extra_specs =\ {'netapp_dedup': 'true', 'netapp:raid_type': 'raid4', 'netapp:disk_type': 'SSD'} - res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs) + res = ssc_cmode.get_volumes_for_specs(ssc_map, extra_specs) self.assertEqual(len(res), 1) def test_query_cl_vols_for_ssc(self): na_server = api.NaServer('127.0.0.1') na_server.set_api_version(1, 15) - vols = ssc_utils.query_cluster_vols_for_ssc(na_server, 'Openstack') + vols = ssc_cmode.query_cluster_vols_for_ssc(na_server, 'Openstack') self.assertEqual(len(vols), 2) for vol in vols: if vol.id['name'] != 'iscsi' or vol.id['name'] != 'nfsvol': @@ -530,7 +529,7 @@ class SscUtilsTestCase(test.TestCase): def test_query_aggr_options(self): na_server = api.NaServer('127.0.0.1') - aggr_attribs = ssc_utils.query_aggr_options(na_server, 'aggr0') + aggr_attribs = ssc_cmode.query_aggr_options(na_server, 'aggr0') if aggr_attribs: self.assertEqual(aggr_attribs['ha_policy'], 'cfo') self.assertEqual(aggr_attribs['raid_type'], 'raid_dp') @@ -539,5 +538,5 @@ class SscUtilsTestCase(test.TestCase): def test_query_aggr_storage_disk(self): na_server = api.NaServer('127.0.0.1') - eff_disk_type = ssc_utils.query_aggr_storage_disk(na_server, 'aggr0') + eff_disk_type = ssc_cmode.query_aggr_storage_disk(na_server, 'aggr0') self.assertEqual(eff_disk_type, 'SATA') diff --git a/cinder/tests/test_netapp_utils.py b/cinder/tests/test_netapp_utils.py deleted file mode 100644 index 6d2d4c9b592..00000000000 --- a/cinder/tests/test_netapp_utils.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright 2014 Tom Barron. 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 -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import platform - -import mock -from oslo.concurrency import processutils as putils - -from cinder import test -from cinder import version -from cinder.volume.drivers.netapp import utils as na_utils - - -class OpenstackInfoTestCase(test.TestCase): - - UNKNOWN_VERSION = 'unknown version' - UNKNOWN_RELEASE = 'unknown release' - UNKNOWN_VENDOR = 'unknown vendor' - UNKNOWN_PLATFORM = 'unknown platform' - VERSION_STRING_RET_VAL = 'fake_version_1' - RELEASE_STRING_RET_VAL = 'fake_release_1' - PLATFORM_RET_VAL = 'fake_platform_1' - VERSION_INFO_VERSION = 'fake_version_2' - VERSION_INFO_RELEASE = 'fake_release_2' - RPM_INFO_VERSION = 'fake_version_3' - RPM_INFO_RELEASE = 'fake_release_3' - RPM_INFO_VENDOR = 'fake vendor 3' - PUTILS_RPM_RET_VAL = ('fake_version_3 fake_release_3 fake vendor 3', '') - NO_PKG_FOUND = ('', 'whatever') - PUTILS_DPKG_RET_VAL = ('epoch:upstream_version-debian_revision', '') - DEB_RLS = 'upstream_version-debian_revision' - DEB_VENDOR = 'debian_revision' - - def setUp(self): - super(OpenstackInfoTestCase, self).setUp() - - def test_openstack_info_init(self): - info = na_utils.OpenStackInfo() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(version.version_info, 'version_string', - mock.Mock(return_value=VERSION_STRING_RET_VAL)) - def test_update_version_from_version_string(self): - info = na_utils.OpenStackInfo() - info._update_version_from_version_string() - - self.assertEqual(self.VERSION_STRING_RET_VAL, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(version.version_info, 'version_string', - mock.Mock(side_effect=Exception)) - def test_xcption_in_update_version_from_version_string(self): - info = na_utils.OpenStackInfo() - info._update_version_from_version_string() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(version.version_info, 'release_string', - mock.Mock(return_value=RELEASE_STRING_RET_VAL)) - def test_update_release_from_release_string(self): - info = na_utils.OpenStackInfo() - info._update_release_from_release_string() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.RELEASE_STRING_RET_VAL, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(version.version_info, 'release_string', - mock.Mock(side_effect=Exception)) - def test_xcption_in_update_release_from_release_string(self): - info = na_utils.OpenStackInfo() - info._update_release_from_release_string() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(platform, 'platform', - mock.Mock(return_value=PLATFORM_RET_VAL)) - def test_update_platform(self): - info = na_utils.OpenStackInfo() - info._update_platform() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.PLATFORM_RET_VAL, info._platform) - - @mock.patch.object(platform, 'platform', - mock.Mock(side_effect=Exception)) - def test_xcption_in_update_platform(self): - info = na_utils.OpenStackInfo() - info._update_platform() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', - mock.Mock(return_value=VERSION_INFO_VERSION)) - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', - mock.Mock(return_value=VERSION_INFO_RELEASE)) - def test_update_info_from_version_info(self): - info = na_utils.OpenStackInfo() - info._update_info_from_version_info() - - self.assertEqual(self.VERSION_INFO_VERSION, info._version) - self.assertEqual(self.VERSION_INFO_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', - mock.Mock(return_value='')) - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', - mock.Mock(return_value=None)) - def test_no_info_from_version_info(self): - info = na_utils.OpenStackInfo() - info._update_info_from_version_info() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', - mock.Mock(return_value=VERSION_INFO_VERSION)) - @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', - mock.Mock(side_effect=Exception)) - def test_xcption_in_info_from_version_info(self): - info = na_utils.OpenStackInfo() - info._update_info_from_version_info() - - self.assertEqual(self.VERSION_INFO_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - - @mock.patch.object(putils, 'execute', - mock.Mock(return_value=PUTILS_RPM_RET_VAL)) - def test_update_info_from_rpm(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_rpm() - - self.assertEqual(self.RPM_INFO_VERSION, info._version) - self.assertEqual(self.RPM_INFO_RELEASE, info._release) - self.assertEqual(self.RPM_INFO_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertTrue(found_package) - - @mock.patch.object(putils, 'execute', - mock.Mock(return_value=NO_PKG_FOUND)) - def test_update_info_from_rpm_no_pkg_found(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_rpm() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertFalse(found_package) - - @mock.patch.object(putils, 'execute', - mock.Mock(side_effect=Exception)) - def test_xcption_in_update_info_from_rpm(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_rpm() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertFalse(found_package) - - @mock.patch.object(putils, 'execute', - mock.Mock(return_value=PUTILS_DPKG_RET_VAL)) - def test_update_info_from_dpkg(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_dpkg() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.DEB_RLS, info._release) - self.assertEqual(self.DEB_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertTrue(found_package) - - @mock.patch.object(putils, 'execute', - mock.Mock(return_value=NO_PKG_FOUND)) - def test_update_info_from_dpkg_no_pkg_found(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_dpkg() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertFalse(found_package) - - @mock.patch.object(putils, 'execute', - mock.Mock(side_effect=Exception)) - def test_xcption_in_update_info_from_dpkg(self): - info = na_utils.OpenStackInfo() - found_package = info._update_info_from_dpkg() - - self.assertEqual(self.UNKNOWN_VERSION, info._version) - self.assertEqual(self.UNKNOWN_RELEASE, info._release) - self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) - self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) - self.assertFalse(found_package) - - @mock.patch.object(na_utils.OpenStackInfo, - '_update_version_from_version_string', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_release_from_release_string', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_platform', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_version_info', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_rpm', mock.Mock(return_value=True)) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_dpkg') - def test_update_openstack_info_rpm_pkg_found(self, mock_updt_from_dpkg): - info = na_utils.OpenStackInfo() - info._update_openstack_info() - - self.assertFalse(mock_updt_from_dpkg.called) - - @mock.patch.object(na_utils.OpenStackInfo, - '_update_version_from_version_string', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_release_from_release_string', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_platform', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_version_info', mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_rpm', mock.Mock(return_value=False)) - @mock.patch.object(na_utils.OpenStackInfo, - '_update_info_from_dpkg') - def test_update_openstack_info_rpm_pkg_not_found(self, - mock_updt_from_dpkg): - info = na_utils.OpenStackInfo() - info._update_openstack_info() - - self.assertTrue(mock_updt_from_dpkg.called) diff --git a/cinder/tests/volume/drivers/netapp/client/__init__.py b/cinder/tests/volume/drivers/netapp/dataontap/client/__init__.py similarity index 100% rename from cinder/tests/volume/drivers/netapp/client/__init__.py rename to cinder/tests/volume/drivers/netapp/dataontap/client/__init__.py diff --git a/cinder/tests/volume/drivers/netapp/dataontap/client/test_api.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_api.py new file mode 100644 index 00000000000..bae4f7a8fac --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_api.py @@ -0,0 +1,155 @@ +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Bob Callaway. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Tests for NetApp API layer +""" + + +from cinder.i18n import _ +from cinder import test +from cinder.volume.drivers.netapp.dataontap.client.api import NaElement +from cinder.volume.drivers.netapp.dataontap.client.api import NaServer + + +class NetAppApiElementTransTests(test.TestCase): + """Test case for NetApp API element translations.""" + + def setUp(self): + super(NetAppApiElementTransTests, self).setUp() + + def test_translate_struct_dict_unique_key(self): + """Tests if dict gets properly converted to NaElements.""" + root = NaElement('root') + child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'} + root.translate_struct(child) + self.assertEqual(len(root.get_children()), 3) + self.assertEqual(root.get_child_content('e1'), 'v1') + self.assertEqual(root.get_child_content('e2'), 'v2') + self.assertEqual(root.get_child_content('e3'), 'v3') + + def test_translate_struct_dict_nonunique_key(self): + """Tests if list/dict gets properly converted to NaElements.""" + root = NaElement('root') + child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}] + root.translate_struct(child) + self.assertEqual(len(root.get_children()), 3) + children = root.get_children() + for c in children: + if c.get_name() == 'e1': + self.assertIn(c.get_content(), ['v1', 'v3']) + else: + self.assertEqual(c.get_content(), 'v2') + + def test_translate_struct_list(self): + """Tests if list gets properly converted to NaElements.""" + root = NaElement('root') + child = ['e1', 'e2'] + root.translate_struct(child) + self.assertEqual(len(root.get_children()), 2) + self.assertIsNone(root.get_child_content('e1')) + self.assertIsNone(root.get_child_content('e2')) + + def test_translate_struct_tuple(self): + """Tests if tuple gets properly converted to NaElements.""" + root = NaElement('root') + child = ('e1', 'e2') + root.translate_struct(child) + self.assertEqual(len(root.get_children()), 2) + self.assertIsNone(root.get_child_content('e1')) + self.assertIsNone(root.get_child_content('e2')) + + def test_translate_invalid_struct(self): + """Tests if invalid data structure raises exception.""" + root = NaElement('root') + child = 'random child element' + self.assertRaises(ValueError, root.translate_struct, child) + + def test_setter_builtin_types(self): + """Tests str, int, float get converted to NaElement.""" + root = NaElement('root') + root['e1'] = 'v1' + root['e2'] = 1 + root['e3'] = 2.0 + root['e4'] = 8l + self.assertEqual(len(root.get_children()), 4) + self.assertEqual(root.get_child_content('e1'), 'v1') + self.assertEqual(root.get_child_content('e2'), '1') + self.assertEqual(root.get_child_content('e3'), '2.0') + self.assertEqual(root.get_child_content('e4'), '8') + + def test_setter_na_element(self): + """Tests na_element gets appended as child.""" + root = NaElement('root') + root['e1'] = NaElement('nested') + self.assertEqual(len(root.get_children()), 1) + e1 = root.get_child_by_name('e1') + self.assertIsInstance(e1, NaElement) + self.assertIsInstance(e1.get_child_by_name('nested'), NaElement) + + def test_setter_child_dict(self): + """Tests dict is appended as child to root.""" + root = NaElement('root') + root['d'] = {'e1': 'v1', 'e2': 'v2'} + e1 = root.get_child_by_name('d') + self.assertIsInstance(e1, NaElement) + sub_ch = e1.get_children() + self.assertEqual(len(sub_ch), 2) + for c in sub_ch: + self.assertIn(c.get_name(), ['e1', 'e2']) + if c.get_name() == 'e1': + self.assertEqual(c.get_content(), 'v1') + else: + self.assertEqual(c.get_content(), 'v2') + + def test_setter_child_list_tuple(self): + """Tests list/tuple are appended as child to root.""" + root = NaElement('root') + root['l'] = ['l1', 'l2'] + root['t'] = ('t1', 't2') + l = root.get_child_by_name('l') + self.assertIsInstance(l, NaElement) + t = root.get_child_by_name('t') + self.assertIsInstance(t, NaElement) + for le in l.get_children(): + self.assertIn(le.get_name(), ['l1', 'l2']) + for te in t.get_children(): + self.assertIn(te.get_name(), ['t1', 't2']) + + def test_setter_no_value(self): + """Tests key with None value.""" + root = NaElement('root') + root['k'] = None + self.assertIsNone(root.get_child_content('k')) + + def test_setter_invalid_value(self): + """Tests invalid value raises exception.""" + root = NaElement('root') + try: + root['k'] = NaServer('localhost') + except Exception as e: + if not isinstance(e, TypeError): + self.fail(_('Error not a TypeError.')) + + def test_setter_invalid_key(self): + """Tests invalid value raises exception.""" + root = NaElement('root') + try: + root[None] = 'value' + except Exception as e: + if not isinstance(e, KeyError): + self.fail(_('Error not a KeyError.')) diff --git a/cinder/tests/volume/drivers/netapp/client/test_seven_mode.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py similarity index 96% rename from cinder/tests/volume/drivers/netapp/client/test_seven_mode.py rename to cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py index e4098697d07..452f413f772 100644 --- a/cinder/tests/volume/drivers/netapp/client/test_seven_mode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py @@ -1,4 +1,4 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -20,17 +20,32 @@ import mock import six from cinder import test -from cinder.volume.drivers.netapp import api as netapp_api -from cinder.volume.drivers.netapp.client import seven_mode +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_7mode + + +CONNECTION_INFO = {'hostname': 'hostname', + 'transport_type': 'https', + 'port': 443, + 'username': 'admin', + 'password': 'passw0rd'} class NetApp7modeClientTestCase(test.TestCase): def setUp(self): super(NetApp7modeClientTestCase, self).setUp() - self.connection = mock.MagicMock() + self.fake_volume = six.text_type(uuid.uuid4()) - self.client = seven_mode.Client(self.connection, [self.fake_volume]) + + with mock.patch.object(client_7mode.Client, + 'get_ontapi_version', + return_value=(1, 20)): + self.client = client_7mode.Client([self.fake_volume], + **CONNECTION_INFO) + + self.client.connection = mock.MagicMock() + self.connection = self.client.connection self.fake_lun = six.text_type(uuid.uuid4()) def tearDown(self): diff --git a/cinder/tests/volume/drivers/netapp/client/test_base.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py similarity index 92% rename from cinder/tests/volume/drivers/netapp/client/test_base.py rename to cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py index 12e6011c44f..b84a53b230c 100644 --- a/cinder/tests/volume/drivers/netapp/client/test_base.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -1,5 +1,4 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# All Rights Reserved. +# Copyright (c) 2014 Alex Meade. 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 @@ -20,23 +19,28 @@ import mock import six from cinder import test -from cinder.volume.drivers.netapp import api as netapp_api -from cinder.volume.drivers.netapp.client import base +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_base + + +CONNECTION_INFO = {'hostname': 'hostname', + 'transport_type': 'https', + 'port': 443, + 'username': 'admin', + 'password': 'passw0rd'} class NetAppBaseClientTestCase(test.TestCase): def setUp(self): super(NetAppBaseClientTestCase, self).setUp() - self.connection = mock.MagicMock() - self.client = base.Client(self.connection) + self.client = client_base.Client(**CONNECTION_INFO) + self.client.connection = mock.MagicMock() + self.connection = self.client.connection self.fake_volume = six.text_type(uuid.uuid4()) self.fake_lun = six.text_type(uuid.uuid4()) self.fake_size = '1024' - self.fake_metadata = { - 'OsType': 'linux', - 'SpaceReserved': 'true', - } + self.fake_metadata = {'OsType': 'linux', 'SpaceReserved': 'true'} def tearDown(self): super(NetAppBaseClientTestCase, self).tearDown() @@ -49,11 +53,25 @@ class NetAppBaseClientTestCase(test.TestCase): """)) self.connection.invoke_successfully.return_value = version_response - major, minor = self.client.get_ontapi_version() + major, minor = self.client.get_ontapi_version(cached=False) self.assertEqual('1', major) self.assertEqual('19', minor) + def test_get_ontapi_version_cached(self): + + self.connection.get_api_version.return_value = (1, 20) + major, minor = self.client.get_ontapi_version() + self.assertEqual(1, self.connection.get_api_version.call_count) + self.assertEqual(1, major) + self.assertEqual(20, minor) + + def test_check_is_naelement(self): + + element = netapp_api.NaElement('name') + self.assertIsNone(self.client.check_is_naelement(element)) + self.assertRaises(ValueError, self.client.check_is_naelement, None) + def test_create_lun(self): expected_path = '/vol/%s/%s' % (self.fake_volume, self.fake_lun) diff --git a/cinder/tests/volume/drivers/netapp/client/test_cmode.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py similarity index 96% rename from cinder/tests/volume/drivers/netapp/client/test_cmode.py rename to cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py index 18bc80892e6..40f74a88846 100644 --- a/cinder/tests/volume/drivers/netapp/client/test_cmode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -1,4 +1,4 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,17 +21,31 @@ import six from cinder import exception from cinder import test -from cinder.volume.drivers.netapp import api as netapp_api -from cinder.volume.drivers.netapp.client import cmode +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_cmode + + +CONNECTION_INFO = {'hostname': 'hostname', + 'transport_type': 'https', + 'port': 443, + 'username': 'admin', + 'password': 'passw0rd', + 'vserver': 'fake_vserver'} class NetAppCmodeClientTestCase(test.TestCase): def setUp(self): super(NetAppCmodeClientTestCase, self).setUp() - self.connection = mock.MagicMock() - self.vserver = 'fake_vserver' - self.client = cmode.Client(self.connection, self.vserver) + + with mock.patch.object(client_cmode.Client, + 'get_ontapi_version', + return_value=(1, 20)): + self.client = client_cmode.Client(**CONNECTION_INFO) + + self.client.connection = mock.MagicMock() + self.connection = self.client.connection + self.vserver = CONNECTION_INFO['vserver'] self.fake_volume = six.text_type(uuid.uuid4()) self.fake_lun = six.text_type(uuid.uuid4()) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py new file mode 100644 index 00000000000..ef5baf92063 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py @@ -0,0 +1,109 @@ +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Mock unit tests for the NetApp block storage 7-mode library +""" + +import uuid + +import mock +import six + +from cinder import test +from cinder.volume.drivers.netapp.dataontap import block_7mode +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api + +FAKE_VOLUME = six.text_type(uuid.uuid4()) +FAKE_LUN = six.text_type(uuid.uuid4()) +FAKE_SIZE = '1024' +FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} + + +class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): + """Test case for NetApp's 7-Mode iSCSI library.""" + + def setUp(self): + super(NetAppBlockStorage7modeLibraryTestCase, self).setUp() + + kwargs = {'configuration': mock.Mock()} + self.library = block_7mode.NetAppBlockStorage7modeLibrary('driver', + 'protocol', + **kwargs) + + self.library.zapi_client = mock.Mock() + self.library.vfiler = mock.Mock() + + def tearDown(self): + super(NetAppBlockStorage7modeLibraryTestCase, self).tearDown() + + def test_clone_lun_zero_block_count(self): + """Test for when clone lun is not passed a block count.""" + + lun = netapp_api.NaElement.create_node_with_children( + 'lun-info', + **{'alignment': 'indeterminate', + 'block-size': '512', + 'comment': '', + 'creation-timestamp': '1354536362', + 'is-space-alloc-enabled': 'false', + 'is-space-reservation-enabled': 'true', + 'mapped': 'false', + 'multiprotocol-type': 'linux', + 'online': 'true', + 'path': '/vol/fakeLUN/fakeLUN', + 'prefix-size': '0', + 'qtree': '', + 'read-only': 'false', + 'serial-number': '2FfGI$APyN68', + 'share-state': 'none', + 'size': '20971520', + 'size-used': '0', + 'staging': 'false', + 'suffix-size': '0', + 'uuid': 'cec1f3d7-3d41-11e2-9cf4-123478563412', + 'volume': 'fakeLUN', + 'vserver': 'fake_vserver'}) + self.library._get_lun_attr = mock.Mock(return_value={ + 'Volume': 'fakeLUN', 'Path': '/vol/fake/fakeLUN'}) + self.library.zapi_client = mock.Mock() + self.library.zapi_client.get_lun_by_args.return_value = [lun] + self.library._add_lun_to_table = mock.Mock() + + self.library._clone_lun('fakeLUN', 'newFakeLUN') + + self.library.zapi_client.clone_lun.assert_called_once_with( + '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', + 'newFakeLUN', 'true', block_count=0, dest_block=0, src_block=0) + + @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary, + '_refresh_volume_info', mock.Mock()) + @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary, + '_get_pool_stats', mock.Mock()) + def test_vol_stats_calls_provide_ems(self): + self.library.zapi_client.provide_ems = mock.Mock() + self.library.get_volume_stats(refresh=True) + self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1) + + def test_create_lun(self): + self.library.vol_refresh_voluntary = False + + self.library._create_lun(FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, + FAKE_METADATA) + + self.library.zapi_client.create_lun.assert_called_once_with( + FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, FAKE_METADATA, None) + + self.assertTrue(self.library.vol_refresh_voluntary) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py new file mode 100644 index 00000000000..62787837a82 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py @@ -0,0 +1,124 @@ +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Mock unit tests for the NetApp block storage library +""" + +import uuid + +import mock + +from cinder import exception +from cinder import test +from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp import utils as na_utils + + +class NetAppBlockStorageLibraryTestCase(test.TestCase): + + def setUp(self): + super(NetAppBlockStorageLibraryTestCase, self).setUp() + + kwargs = {'configuration': mock.Mock()} + self.library = block_base.NetAppBlockStorageLibrary('driver', + 'protocol', + **kwargs) + self.library.zapi_client = mock.Mock() + self.mock_request = mock.Mock() + + def tearDown(self): + super(NetAppBlockStorageLibraryTestCase, self).tearDown() + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + mock.Mock(return_value={'Volume': 'vol1'})) + def test_get_pool(self): + pool = self.library.get_pool({'name': 'volume-fake-uuid'}) + self.assertEqual(pool, 'vol1') + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + mock.Mock(return_value=None)) + def test_get_pool_no_metadata(self): + pool = self.library.get_pool({'name': 'volume-fake-uuid'}) + self.assertEqual(pool, None) + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + mock.Mock(return_value=dict())) + def test_get_pool_volume_unknown(self): + pool = self.library.get_pool({'name': 'volume-fake-uuid'}) + self.assertEqual(pool, None) + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_create_lun', + mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_create_lun_handle', + mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_add_lun_to_table', + mock.Mock()) + @mock.patch.object(na_utils, 'get_volume_extra_specs', + mock.Mock(return_value=None)) + @mock.patch.object(block_base, 'LOG', + mock.Mock()) + def test_create_volume(self): + self.library.create_volume({'name': 'lun1', 'size': 100, + 'id': uuid.uuid4(), + 'host': 'hostname@backend#vol1'}) + self.library._create_lun.assert_called_once_with( + 'vol1', 'lun1', 107374182400, mock.ANY, None) + self.assertEqual(0, block_base.LOG.warn.call_count) + + def test_create_volume_no_pool_provided_by_scheduler(self): + self.assertRaises(exception.InvalidHost, self.library.create_volume, + {'name': 'lun1', 'size': 100, + 'id': uuid.uuid4(), + 'host': 'hostname@backend'}) # missing pool + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_create_lun', mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_create_lun_handle', mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_add_lun_to_table', mock.Mock()) + @mock.patch.object(na_utils, 'LOG', mock.Mock()) + @mock.patch.object(na_utils, 'get_volume_extra_specs', + mock.Mock(return_value={'netapp:raid_type': 'raid4'})) + def test_create_volume_obsolete_extra_spec(self): + + self.library.create_volume({'name': 'lun1', 'size': 100, + 'id': uuid.uuid4(), + 'host': 'hostname@backend#vol1'}) + warn_msg = 'Extra spec netapp:raid_type is obsolete. ' \ + 'Use netapp_raid_type instead.' + na_utils.LOG.warn.assert_called_once_with(warn_msg) + + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_create_lun', mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_create_lun_handle', mock.Mock()) + @mock.patch.object(block_base.NetAppBlockStorageLibrary, + '_add_lun_to_table', mock.Mock()) + @mock.patch.object(na_utils, 'LOG', mock.Mock()) + @mock.patch.object(na_utils, 'get_volume_extra_specs', + mock.Mock(return_value={'netapp_thick_provisioned': + 'true'})) + def test_create_volume_deprecated_extra_spec(self): + + self.library.create_volume({'name': 'lun1', 'size': 100, + 'id': uuid.uuid4(), + 'host': 'hostname@backend#vol1'}) + warn_msg = 'Extra spec netapp_thick_provisioned is deprecated. ' \ + 'Use netapp_thin_provisioned instead.' + na_utils.LOG.warn.assert_called_once_with(warn_msg) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py new file mode 100644 index 00000000000..39f29648fe4 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -0,0 +1,115 @@ +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Mock unit tests for the NetApp block storage C-mode library +""" + +import uuid + +import mock +import six + +from cinder import test +from cinder.volume.drivers.netapp.dataontap import block_cmode +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap import ssc_cmode + +FAKE_VOLUME = six.text_type(uuid.uuid4()) +FAKE_LUN = six.text_type(uuid.uuid4()) +FAKE_SIZE = '1024' +FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} + + +class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): + """Test case for NetApp's C-Mode iSCSI library.""" + + def setUp(self): + super(NetAppBlockStorageCmodeLibraryTestCase, self).setUp() + + kwargs = {'configuration': mock.Mock()} + self.library = block_cmode.NetAppBlockStorageCmodeLibrary('driver', + 'protocol', + **kwargs) + self.library.zapi_client = mock.Mock() + self.library.vserver = mock.Mock() + self.library.ssc_vols = None + + def tearDown(self): + super(NetAppBlockStorageCmodeLibraryTestCase, self).tearDown() + + def test_clone_lun_zero_block_count(self): + """Test for when clone lun is not passed a block count.""" + + self.library._get_lun_attr = mock.Mock(return_value={'Volume': + 'fakeLUN'}) + self.library.zapi_client = mock.Mock() + self.library.zapi_client.get_lun_by_args.return_value = [ + mock.Mock(spec=netapp_api.NaElement)] + lun = netapp_api.NaElement.create_node_with_children( + 'lun-info', + **{'alignment': 'indeterminate', + 'block-size': '512', + 'comment': '', + 'creation-timestamp': '1354536362', + 'is-space-alloc-enabled': 'false', + 'is-space-reservation-enabled': 'true', + 'mapped': 'false', + 'multiprotocol-type': 'linux', + 'online': 'true', + 'path': '/vol/fakeLUN/lun1', + 'prefix-size': '0', + 'qtree': '', + 'read-only': 'false', + 'serial-number': '2FfGI$APyN68', + 'share-state': 'none', + 'size': '20971520', + 'size-used': '0', + 'staging': 'false', + 'suffix-size': '0', + 'uuid': 'cec1f3d7-3d41-11e2-9cf4-123478563412', + 'volume': 'fakeLUN', + 'vserver': 'fake_vserver'}) + self.library._get_lun_by_args = mock.Mock(return_value=[lun]) + self.library._add_lun_to_table = mock.Mock() + self.library._update_stale_vols = mock.Mock() + + self.library._clone_lun('fakeLUN', 'newFakeLUN') + + self.library.zapi_client.clone_lun.assert_called_once_with( + 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'true', block_count=0, + dest_block=0, src_block=0) + + @mock.patch.object(ssc_cmode, 'refresh_cluster_ssc', mock.Mock()) + @mock.patch.object(block_cmode.NetAppBlockStorageCmodeLibrary, + '_get_pool_stats', mock.Mock()) + def test_vol_stats_calls_provide_ems(self): + self.library.zapi_client.provide_ems = mock.Mock() + self.library.get_volume_stats(refresh=True) + self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1) + + def test_create_lun(self): + self.library._update_stale_vols = mock.Mock() + + self.library._create_lun(FAKE_VOLUME, + FAKE_LUN, + FAKE_SIZE, + FAKE_METADATA) + + self.library.zapi_client.create_lun.assert_called_once_with( + FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, + FAKE_METADATA, None) + + self.assertEqual(1, self.library._update_stale_vols.call_count) diff --git a/cinder/tests/volume/drivers/netapp/eseries/test_utils.py b/cinder/tests/volume/drivers/netapp/eseries/test_utils.py new file mode 100644 index 00000000000..51ece9939e2 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/eseries/test_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Mock unit tests for the NetApp E-series driver utility module +""" + +import six + +from cinder import test +from cinder.volume.drivers.netapp.eseries import utils + + +class NetAppEseriesDriverUtilsTestCase(test.TestCase): + + def test_convert_uuid_to_es_fmt(self): + value = 'e67e931a-b2ed-4890-938b-3acc6a517fac' + result = utils.convert_uuid_to_es_fmt(value) + self.assertEqual(result, '4Z7JGGVS5VEJBE4LHLGGUUL7VQ') + + def test_convert_es_fmt_to_uuid(self): + value = '4Z7JGGVS5VEJBE4LHLGGUUL7VQ' + result = six.text_type(utils.convert_es_fmt_to_uuid(value)) + self.assertEqual(result, 'e67e931a-b2ed-4890-938b-3acc6a517fac') diff --git a/cinder/tests/volume/drivers/netapp/test_iscsi.py b/cinder/tests/volume/drivers/netapp/test_iscsi.py deleted file mode 100644 index 887a231ba15..00000000000 --- a/cinder/tests/volume/drivers/netapp/test_iscsi.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# 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 -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Mock unit tests for the NetApp iSCSI driver -""" - -import uuid - -import mock -import six - -from cinder import exception -from cinder.i18n import _ -from cinder import test -from cinder.tests.test_netapp import create_configuration -import cinder.volume.drivers.netapp.api as ntapi -import cinder.volume.drivers.netapp.iscsi as ntap_iscsi -from cinder.volume.drivers.netapp.iscsi import NetAppDirect7modeISCSIDriver \ - as iscsi7modeDriver -from cinder.volume.drivers.netapp.iscsi import NetAppDirectCmodeISCSIDriver \ - as iscsiCmodeDriver -from cinder.volume.drivers.netapp.iscsi import NetAppDirectISCSIDriver \ - as iscsiDriver -import cinder.volume.drivers.netapp.ssc_utils as ssc_utils -import cinder.volume.drivers.netapp.utils as na_utils - - -FAKE_VOLUME = six.text_type(uuid.uuid4()) -FAKE_LUN = six.text_type(uuid.uuid4()) -FAKE_SIZE = '1024' -FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} - - -class NetAppDirectISCSIDriverTestCase(test.TestCase): - - def setUp(self): - super(NetAppDirectISCSIDriverTestCase, self).setUp() - configuration = self._set_config(create_configuration()) - self.driver = ntap_iscsi.NetAppDirectISCSIDriver( - configuration=configuration) - self.driver.client = mock.Mock() - self.driver.zapi_client = mock.Mock() - self.mock_request = mock.Mock() - - def _set_config(self, configuration): - configuration.netapp_storage_protocol = 'iscsi' - configuration.netapp_login = 'admin' - configuration.netapp_password = 'pass' - configuration.netapp_server_hostname = '127.0.0.1' - configuration.netapp_transport_type = 'http' - configuration.netapp_server_port = '80' - return configuration - - def tearDown(self): - super(NetAppDirectISCSIDriverTestCase, self).tearDown() - - @mock.patch.object(iscsiDriver, '_get_lun_attr', - mock.Mock(return_value={'Volume': 'vol1'})) - def test_get_pool(self): - pool = self.driver.get_pool({'name': 'volume-fake-uuid'}) - self.assertEqual(pool, 'vol1') - - @mock.patch.object(iscsiDriver, '_get_lun_attr', - mock.Mock(return_value=None)) - def test_get_pool_no_metadata(self): - pool = self.driver.get_pool({'name': 'volume-fake-uuid'}) - self.assertEqual(pool, None) - - @mock.patch.object(iscsiDriver, '_get_lun_attr', - mock.Mock(return_value=dict())) - def test_get_pool_volume_unknown(self): - pool = self.driver.get_pool({'name': 'volume-fake-uuid'}) - self.assertEqual(pool, None) - - @mock.patch.object(iscsiDriver, 'create_lun', mock.Mock()) - @mock.patch.object(iscsiDriver, '_create_lun_handle', mock.Mock()) - @mock.patch.object(iscsiDriver, '_add_lun_to_table', mock.Mock()) - @mock.patch.object(ntap_iscsi, 'LOG', mock.Mock()) - @mock.patch.object(ntap_iscsi, 'get_volume_extra_specs', - mock.Mock(return_value=None)) - def test_create_volume(self): - self.driver.create_volume({'name': 'lun1', 'size': 100, - 'id': uuid.uuid4(), - 'host': 'hostname@backend#vol1'}) - self.driver.create_lun.assert_called_once_with( - 'vol1', 'lun1', 107374182400, mock.ANY, None) - self.assertEqual(0, ntap_iscsi.LOG.warn.call_count) - - def test_create_volume_no_pool_provided_by_scheduler(self): - self.assertRaises(exception.InvalidHost, self.driver.create_volume, - {'name': 'lun1', 'size': 100, - 'id': uuid.uuid4(), - 'host': 'hostname@backend'}) # missing pool - - @mock.patch.object(iscsiDriver, 'create_lun', mock.Mock()) - @mock.patch.object(iscsiDriver, '_create_lun_handle', mock.Mock()) - @mock.patch.object(iscsiDriver, '_add_lun_to_table', mock.Mock()) - @mock.patch.object(na_utils, 'LOG', mock.Mock()) - @mock.patch.object(ntap_iscsi, 'get_volume_extra_specs', - mock.Mock(return_value={'netapp:raid_type': 'raid4'})) - def test_create_volume_obsolete_extra_spec(self): - - self.driver.create_volume({'name': 'lun1', 'size': 100, - 'id': uuid.uuid4(), - 'host': 'hostname@backend#vol1'}) - warn_msg = 'Extra spec netapp:raid_type is obsolete. ' \ - 'Use netapp_raid_type instead.' - na_utils.LOG.warning.assert_called_once_with(warn_msg) - - @mock.patch.object(iscsiDriver, 'create_lun', mock.Mock()) - @mock.patch.object(iscsiDriver, '_create_lun_handle', mock.Mock()) - @mock.patch.object(iscsiDriver, '_add_lun_to_table', mock.Mock()) - @mock.patch.object(na_utils, 'LOG', mock.Mock()) - @mock.patch.object(ntap_iscsi, 'get_volume_extra_specs', - mock.Mock(return_value={'netapp_thick_provisioned': - 'true'})) - def test_create_volume_deprecated_extra_spec(self): - - self.driver.create_volume({'name': 'lun1', 'size': 100, - 'id': uuid.uuid4(), - 'host': 'hostname@backend#vol1'}) - warn_msg = 'Extra spec netapp_thick_provisioned is deprecated. ' \ - 'Use netapp_thin_provisioned instead.' - na_utils.LOG.warning.assert_called_once_with(warn_msg) - - def test_update_volume_stats_is_abstract(self): - self.assertRaises(NotImplementedError, - self.driver._update_volume_stats) - - def test_initialize_connection_no_target_details_found(self): - fake_volume = {'name': 'mock-vol'} - fake_connector = {'initiator': 'iqn.mock'} - self.driver._map_lun = mock.Mock(return_value='mocked-lun-id') - self.driver.zapi_client.get_iscsi_service_details = mock.Mock( - return_value='mocked-iqn') - self.driver.zapi_client.get_target_details = mock.Mock(return_value=[]) - expected = (_('No iscsi target details were found for LUN %s') - % fake_volume['name']) - try: - self.driver.initialize_connection(fake_volume, fake_connector) - except exception.VolumeBackendAPIException as exc: - if expected not in six.text_type(exc): - self.fail(_('Expected exception message is missing')) - else: - self.fail(_('VolumeBackendAPIException not raised')) - - -class NetAppiSCSICModeTestCase(test.TestCase): - """Test case for NetApp's C-Mode iSCSI driver.""" - - def setUp(self): - super(NetAppiSCSICModeTestCase, self).setUp() - self.driver = ntap_iscsi.NetAppDirectCmodeISCSIDriver( - configuration=mock.Mock()) - self.driver.client = mock.Mock() - self.driver.zapi_client = mock.Mock() - self.driver.vserver = mock.Mock() - self.driver.ssc_vols = None - - def tearDown(self): - super(NetAppiSCSICModeTestCase, self).tearDown() - - def test_clone_lun_zero_block_count(self): - """Test for when clone lun is not passed a block count.""" - - self.driver._get_lun_attr = mock.Mock(return_value={'Volume': - 'fakeLUN'}) - self.driver.zapi_client = mock.Mock() - self.driver.zapi_client.get_lun_by_args.return_value = [ - mock.Mock(spec=ntapi.NaElement)] - lun = ntapi.NaElement.create_node_with_children( - 'lun-info', - **{'alignment': 'indeterminate', - 'block-size': '512', - 'comment': '', - 'creation-timestamp': '1354536362', - 'is-space-alloc-enabled': 'false', - 'is-space-reservation-enabled': 'true', - 'mapped': 'false', - 'multiprotocol-type': 'linux', - 'online': 'true', - 'path': '/vol/fakeLUN/lun1', - 'prefix-size': '0', - 'qtree': '', - 'read-only': 'false', - 'serial-number': '2FfGI$APyN68', - 'share-state': 'none', - 'size': '20971520', - 'size-used': '0', - 'staging': 'false', - 'suffix-size': '0', - 'uuid': 'cec1f3d7-3d41-11e2-9cf4-123478563412', - 'volume': 'fakeLUN', - 'vserver': 'fake_vserver'}) - self.driver._get_lun_by_args = mock.Mock(return_value=[lun]) - self.driver._add_lun_to_table = mock.Mock() - self.driver._update_stale_vols = mock.Mock() - - self.driver._clone_lun('fakeLUN', 'newFakeLUN') - - self.driver.zapi_client.clone_lun.assert_called_once_with( - 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'true', block_count=0, - dest_block=0, src_block=0) - - @mock.patch.object(ssc_utils, 'refresh_cluster_ssc', mock.Mock()) - @mock.patch.object(iscsiCmodeDriver, '_get_pool_stats', mock.Mock()) - @mock.patch.object(na_utils, 'provide_ems', mock.Mock()) - def test_vol_stats_calls_provide_ems(self): - self.driver.get_volume_stats(refresh=True) - self.assertEqual(na_utils.provide_ems.call_count, 1) - - def test_create_lun(self): - self.driver._update_stale_vols = mock.Mock() - - self.driver.create_lun(FAKE_VOLUME, - FAKE_LUN, - FAKE_SIZE, - FAKE_METADATA) - - self.driver.zapi_client.create_lun.assert_called_once_with( - FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, - FAKE_METADATA, None) - - self.assertEqual(1, self.driver._update_stale_vols.call_count) - - -class NetAppiSCSI7ModeTestCase(test.TestCase): - """Test case for NetApp's 7-Mode iSCSI driver.""" - - def setUp(self): - super(NetAppiSCSI7ModeTestCase, self).setUp() - self.driver = ntap_iscsi.NetAppDirect7modeISCSIDriver( - configuration=mock.Mock()) - self.driver.client = mock.Mock() - self.driver.zapi_client = mock.Mock() - self.driver.vfiler = mock.Mock() - - def tearDown(self): - super(NetAppiSCSI7ModeTestCase, self).tearDown() - - def test_clone_lun_zero_block_count(self): - """Test for when clone lun is not passed a block count.""" - - lun = ntapi.NaElement.create_node_with_children( - 'lun-info', - **{'alignment': 'indeterminate', - 'block-size': '512', - 'comment': '', - 'creation-timestamp': '1354536362', - 'is-space-alloc-enabled': 'false', - 'is-space-reservation-enabled': 'true', - 'mapped': 'false', - 'multiprotocol-type': 'linux', - 'online': 'true', - 'path': '/vol/fakeLUN/fakeLUN', - 'prefix-size': '0', - 'qtree': '', - 'read-only': 'false', - 'serial-number': '2FfGI$APyN68', - 'share-state': 'none', - 'size': '20971520', - 'size-used': '0', - 'staging': 'false', - 'suffix-size': '0', - 'uuid': 'cec1f3d7-3d41-11e2-9cf4-123478563412', - 'volume': 'fakeLUN', - 'vserver': 'fake_vserver'}) - self.driver._get_lun_attr = mock.Mock(return_value={ - 'Volume': 'fakeLUN', 'Path': '/vol/fake/fakeLUN'}) - self.driver.zapi_client = mock.Mock() - self.driver.zapi_client.get_lun_by_args.return_value = [lun] - self.driver._add_lun_to_table = mock.Mock() - - self.driver._clone_lun('fakeLUN', 'newFakeLUN') - - self.driver.zapi_client.clone_lun.assert_called_once_with( - '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', - 'newFakeLUN', 'true', block_count=0, dest_block=0, src_block=0) - - @mock.patch.object(iscsi7modeDriver, '_refresh_volume_info', mock.Mock()) - @mock.patch.object(iscsi7modeDriver, '_get_pool_stats', mock.Mock()) - @mock.patch.object(na_utils, 'provide_ems', mock.Mock()) - def test_vol_stats_calls_provide_ems(self): - self.driver.get_volume_stats(refresh=True) - self.assertEqual(na_utils.provide_ems.call_count, 1) - - def test_create_lun(self): - self.driver.vol_refresh_voluntary = False - - self.driver.create_lun(FAKE_VOLUME, - FAKE_LUN, - FAKE_SIZE, - FAKE_METADATA) - - self.driver.zapi_client.create_lun.assert_called_once_with( - FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, - FAKE_METADATA, None) - - self.assertTrue(self.driver.vol_refresh_voluntary) diff --git a/cinder/tests/volume/drivers/netapp/test_utils.py b/cinder/tests/volume/drivers/netapp/test_utils.py index 8e2e231b389..271bca97f7e 100644 --- a/cinder/tests/volume/drivers/netapp/test_utils.py +++ b/cinder/tests/volume/drivers/netapp/test_utils.py @@ -1,4 +1,5 @@ -# Copyright (c) Clinton Knight +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Tom Barron. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,14 +17,46 @@ Mock unit tests for the NetApp driver utility module """ -import six +import platform +import mock +from oslo.concurrency import processutils as putils + +from cinder import exception from cinder import test +from cinder import version import cinder.volume.drivers.netapp.utils as na_utils class NetAppDriverUtilsTestCase(test.TestCase): + @mock.patch.object(na_utils, 'LOG', mock.Mock()) + def test_validate_instantiation_proxy(self): + kwargs = {'netapp_mode': 'proxy'} + na_utils.validate_instantiation(**kwargs) + self.assertEqual(na_utils.LOG.warning.call_count, 0) + + @mock.patch.object(na_utils, 'LOG', mock.Mock()) + def test_validate_instantiation_no_proxy(self): + kwargs = {'netapp_mode': 'asdf'} + na_utils.validate_instantiation(**kwargs) + self.assertEqual(na_utils.LOG.warning.call_count, 1) + + def test_check_flags(self): + + class TestClass(object): + pass + + required_flags = ['flag1', 'flag2'] + configuration = TestClass() + setattr(configuration, 'flag1', 'value1') + setattr(configuration, 'flag3', 'value3') + self.assertRaises(exception.InvalidInput, na_utils.check_flags, + required_flags, configuration) + + setattr(configuration, 'flag2', 'value2') + self.assertIsNone(na_utils.check_flags(required_flags, configuration)) + def test_to_bool(self): self.assertTrue(na_utils.to_bool(True)) self.assertTrue(na_utils.to_bool('true')) @@ -41,15 +74,27 @@ class NetAppDriverUtilsTestCase(test.TestCase): self.assertFalse(na_utils.to_bool(2)) self.assertFalse(na_utils.to_bool('2')) - def test_convert_uuid_to_es_fmt(self): - value = 'e67e931a-b2ed-4890-938b-3acc6a517fac' - result = na_utils.convert_uuid_to_es_fmt(value) - self.assertEqual(result, '4Z7JGGVS5VEJBE4LHLGGUUL7VQ') + def test_set_safe_attr(self): - def test_convert_es_fmt_to_uuid(self): - value = '4Z7JGGVS5VEJBE4LHLGGUUL7VQ' - result = six.text_type(na_utils.convert_es_fmt_to_uuid(value)) - self.assertEqual(result, 'e67e931a-b2ed-4890-938b-3acc6a517fac') + fake_object = mock.Mock() + fake_object.fake_attr = None + + # test initial checks + self.assertFalse(na_utils.set_safe_attr(None, fake_object, None)) + self.assertFalse(na_utils.set_safe_attr(fake_object, None, None)) + self.assertFalse(na_utils.set_safe_attr(fake_object, 'fake_attr', + None)) + + # test value isn't changed if it shouldn't be and retval is False + fake_object.fake_attr = 'fake_value' + self.assertFalse(na_utils.set_safe_attr(fake_object, 'fake_attr', + 'fake_value')) + self.assertEqual(fake_object.fake_attr, 'fake_value') + + # test value is changed if it should be and retval is True + self.assertTrue(na_utils.set_safe_attr(fake_object, 'fake_attr', + 'new_fake_value')) + self.assertEqual(fake_object.fake_attr, 'new_fake_value') def test_round_down(self): self.assertAlmostEqual(na_utils.round_down(5.567, '0.00'), 5.56) @@ -59,3 +104,249 @@ class NetAppDriverUtilsTestCase(test.TestCase): self.assertAlmostEqual(na_utils.round_down(-5.567, '0.00'), -5.56) self.assertAlmostEqual(na_utils.round_down(-5.567, '0.0'), -5.5) self.assertAlmostEqual(na_utils.round_down(-5.567, '0'), -5) + + +class OpenStackInfoTestCase(test.TestCase): + + UNKNOWN_VERSION = 'unknown version' + UNKNOWN_RELEASE = 'unknown release' + UNKNOWN_VENDOR = 'unknown vendor' + UNKNOWN_PLATFORM = 'unknown platform' + VERSION_STRING_RET_VAL = 'fake_version_1' + RELEASE_STRING_RET_VAL = 'fake_release_1' + PLATFORM_RET_VAL = 'fake_platform_1' + VERSION_INFO_VERSION = 'fake_version_2' + VERSION_INFO_RELEASE = 'fake_release_2' + RPM_INFO_VERSION = 'fake_version_3' + RPM_INFO_RELEASE = 'fake_release_3' + RPM_INFO_VENDOR = 'fake vendor 3' + PUTILS_RPM_RET_VAL = ('fake_version_3 fake_release_3 fake vendor 3', '') + NO_PKG_FOUND = ('', 'whatever') + PUTILS_DPKG_RET_VAL = ('epoch:upstream_version-debian_revision', '') + DEB_RLS = 'upstream_version-debian_revision' + DEB_VENDOR = 'debian_revision' + + def setUp(self): + super(OpenStackInfoTestCase, self).setUp() + + def test_openstack_info_init(self): + info = na_utils.OpenStackInfo() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'version_string', + mock.Mock(return_value=VERSION_STRING_RET_VAL)) + def test_update_version_from_version_string(self): + info = na_utils.OpenStackInfo() + info._update_version_from_version_string() + + self.assertEqual(self.VERSION_STRING_RET_VAL, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'version_string', + mock.Mock(side_effect=Exception)) + def test_xcption_in_update_version_from_version_string(self): + info = na_utils.OpenStackInfo() + info._update_version_from_version_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'release_string', + mock.Mock(return_value=RELEASE_STRING_RET_VAL)) + def test_update_release_from_release_string(self): + info = na_utils.OpenStackInfo() + info._update_release_from_release_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.RELEASE_STRING_RET_VAL, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'release_string', + mock.Mock(side_effect=Exception)) + def test_xcption_in_update_release_from_release_string(self): + info = na_utils.OpenStackInfo() + info._update_release_from_release_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(platform, 'platform', + mock.Mock(return_value=PLATFORM_RET_VAL)) + def test_update_platform(self): + info = na_utils.OpenStackInfo() + info._update_platform() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.PLATFORM_RET_VAL, info._platform) + + @mock.patch.object(platform, 'platform', + mock.Mock(side_effect=Exception)) + def test_xcption_in_update_platform(self): + info = na_utils.OpenStackInfo() + info._update_platform() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value=VERSION_INFO_VERSION)) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(return_value=VERSION_INFO_RELEASE)) + def test_update_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.VERSION_INFO_VERSION, info._version) + self.assertEqual(self.VERSION_INFO_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value='')) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(return_value=None)) + def test_no_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value=VERSION_INFO_VERSION)) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(side_effect=Exception)) + def test_xcption_in_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.VERSION_INFO_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=PUTILS_RPM_RET_VAL)) + def test_update_info_from_rpm(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.RPM_INFO_VERSION, info._version) + self.assertEqual(self.RPM_INFO_RELEASE, info._release) + self.assertEqual(self.RPM_INFO_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertTrue(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=NO_PKG_FOUND)) + def test_update_info_from_rpm_no_pkg_found(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(side_effect=Exception)) + def test_xcption_in_update_info_from_rpm(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=PUTILS_DPKG_RET_VAL)) + def test_update_info_from_dpkg(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.DEB_RLS, info._release) + self.assertEqual(self.DEB_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertTrue(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=NO_PKG_FOUND)) + def test_update_info_from_dpkg_no_pkg_found(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(side_effect=Exception)) + def test_xcption_in_update_info_from_dpkg(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(na_utils.OpenStackInfo, + '_update_version_from_version_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_release_from_release_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_platform', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_version_info', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_rpm', mock.Mock(return_value=True)) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_dpkg') + def test_update_openstack_info_rpm_pkg_found(self, mock_updt_from_dpkg): + info = na_utils.OpenStackInfo() + info._update_openstack_info() + + self.assertFalse(mock_updt_from_dpkg.called) + + @mock.patch.object(na_utils.OpenStackInfo, + '_update_version_from_version_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_release_from_release_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_platform', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_version_info', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_rpm', mock.Mock(return_value=False)) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_dpkg') + def test_update_openstack_info_rpm_pkg_not_found(self, + mock_updt_from_dpkg): + info = na_utils.OpenStackInfo() + info._update_openstack_info() + + self.assertTrue(mock_updt_from_dpkg.called) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 162dd73a4fb..36e2b7ef232 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -1,6 +1,6 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# All Rights Reserved. +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -21,11 +21,11 @@ Supports call to multiple storage systems of different families and protocols. from oslo.utils import importutils from cinder import exception -from cinder.i18n import _ +from cinder.i18n import _, _LI from cinder.openstack.common import log as logging from cinder.volume import driver from cinder.volume.drivers.netapp.options import netapp_proxy_opts -from cinder.volume.drivers.netapp import utils +from cinder.volume.drivers.netapp import utils as na_utils LOG = logging.getLogger(__name__) @@ -34,37 +34,27 @@ LOG = logging.getLogger(__name__) # NOTE(singn): Holds family:{protocol:driver} registration information. # Plug in new families and protocols to support new drivers. # No other code modification required. + +DATAONTAP_PATH = 'cinder.volume.drivers.netapp.dataontap' +ESERIES_PATH = 'cinder.volume.drivers.netapp.eseries' + netapp_unified_plugin_registry =\ {'ontap_cluster': { - 'iscsi': - 'cinder.volume.drivers.netapp.iscsi.NetAppDirectCmodeISCSIDriver', - 'nfs': 'cinder.volume.drivers.netapp.nfs.NetAppDirectCmodeNfsDriver' + 'iscsi': DATAONTAP_PATH + '.iscsi_cmode.NetAppCmodeISCSIDriver', + 'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver' }, 'ontap_7mode': { - 'iscsi': - 'cinder.volume.drivers.netapp.iscsi.NetAppDirect7modeISCSIDriver', - 'nfs': - 'cinder.volume.drivers.netapp.nfs.NetAppDirect7modeNfsDriver' + 'iscsi': DATAONTAP_PATH + '.iscsi_7mode.NetApp7modeISCSIDriver', + 'nfs': DATAONTAP_PATH + '.nfs_7mode.NetApp7modeNfsDriver' }, 'eseries': { - 'iscsi': - 'cinder.volume.drivers.netapp.eseries.iscsi.Driver' + 'iscsi': ESERIES_PATH + '.iscsi.NetAppEseriesISCSIDriver' }, } -# NOTE(singn): Holds family:protocol information. -# Protocol represents the default protocol driver option -# in case no protocol is specified by the user in configuration. -netapp_family_default =\ - { - 'ontap_cluster': 'nfs', - 'ontap_7mode': 'nfs', - 'eseries': 'iscsi' - } - class NetAppDriver(object): """"NetApp unified block storage driver. @@ -74,18 +64,25 @@ class NetAppDriver(object): Override the proxy driver method by adding method in this driver. """ + REQUIRED_FLAGS = ['netapp_storage_family', 'netapp_storage_protocol'] + def __init__(self, *args, **kwargs): super(NetAppDriver, self).__init__() - app_version = utils.OpenStackInfo().info() - LOG.info(_('OpenStack OS Version Info: %(info)s') % { + + app_version = na_utils.OpenStackInfo().info() + LOG.info(_LI('OpenStack OS Version Info: %(info)s') % { 'info': app_version}) + self.configuration = kwargs.get('configuration', None) - if self.configuration: - self.configuration.append_config_values(netapp_proxy_opts) - else: + if not self.configuration: raise exception.InvalidInput( reason=_("Required configuration not found")) + + self.configuration.append_config_values(netapp_proxy_opts) + na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) + kwargs['app_version'] = app_version + self.driver = NetAppDriverFactory.create_driver( self.configuration.netapp_storage_family, self.configuration.netapp_storage_protocol, @@ -108,40 +105,33 @@ class NetAppDriverFactory(object): """Factory to instantiate appropriate NetApp driver.""" @staticmethod - def create_driver( - storage_family, storage_protocol, *args, **kwargs): + def create_driver(storage_family, storage_protocol, *args, **kwargs): """"Creates an appropriate driver based on family and protocol.""" - fmt = {'storage_family': storage_family, - 'storage_protocol': storage_protocol} - LOG.info(_('Requested unified config: %(storage_family)s and ' - '%(storage_protocol)s') % fmt) - storage_family = storage_family.lower() + + fmt = {'storage_family': storage_family.lower(), + 'storage_protocol': storage_protocol.lower()} + LOG.info(_LI('Requested unified config: %(storage_family)s and ' + '%(storage_protocol)s') % fmt) + family_meta = netapp_unified_plugin_registry.get(storage_family) if family_meta is None: raise exception.InvalidInput( reason=_('Storage family %s is not supported') % storage_family) - if storage_protocol is None: - storage_protocol = netapp_family_default.get(storage_family) - fmt['storage_protocol'] = storage_protocol - if storage_protocol is None: - raise exception.InvalidInput( - reason=_('No default storage protocol found' - ' for storage family %(storage_family)s') - % fmt) - storage_protocol = storage_protocol.lower() + driver_loc = family_meta.get(storage_protocol) if driver_loc is None: raise exception.InvalidInput( reason=_('Protocol %(storage_protocol)s is not supported' ' for storage family %(storage_family)s') % fmt) + NetAppDriverFactory.check_netapp_driver(driver_loc) kwargs = kwargs or {} kwargs['netapp_mode'] = 'proxy' driver = importutils.import_object(driver_loc, *args, **kwargs) - LOG.info(_('NetApp driver of family %(storage_family)s and protocol' - ' %(storage_protocol)s loaded') % fmt) + LOG.info(_LI('NetApp driver of family %(storage_family)s and protocol' + ' %(storage_protocol)s loaded') % fmt) return driver @staticmethod diff --git a/cinder/volume/drivers/netapp/client/__init__.py b/cinder/volume/drivers/netapp/dataontap/__init__.py similarity index 100% rename from cinder/volume/drivers/netapp/client/__init__.py rename to cinder/volume/drivers/netapp/dataontap/__init__.py diff --git a/cinder/volume/drivers/netapp/dataontap/block_7mode.py b/cinder/volume/drivers/netapp/dataontap/block_7mode.py new file mode 100644 index 00000000000..a83776f62a3 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/block_7mode.py @@ -0,0 +1,283 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Andrew Kerr. All rights reserved. +# Copyright (c) 2014 Jeff Applewhite. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver library for NetApp 7-mode block storage systems. +""" + +from oslo.utils import timeutils +from oslo.utils import units +import six + +from cinder import exception +from cinder.i18n import _, _LW +from cinder.openstack.common import log as logging +from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp.dataontap.client import client_7mode +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils + + +LOG = logging.getLogger(__name__) + + +class NetAppBlockStorage7modeLibrary(block_base. + NetAppBlockStorageLibrary): + """NetApp block storage library for Data ONTAP (7-mode).""" + + def __init__(self, driver_name, driver_protocol, **kwargs): + super(NetAppBlockStorage7modeLibrary, self).__init__(driver_name, + driver_protocol, + **kwargs) + self.configuration.append_config_values(na_opts.netapp_7mode_opts) + self.driver_mode = '7mode' + + def do_setup(self, context): + super(NetAppBlockStorage7modeLibrary, self).do_setup(context) + + self.volume_list = self.configuration.netapp_volume_list + if self.volume_list: + self.volume_list = self.volume_list.split(',') + self.volume_list = [el.strip() for el in self.volume_list] + + self.vfiler = self.configuration.netapp_vfiler + + self.zapi_client = client_7mode.Client( + self.volume_list, + transport_type=self.configuration.netapp_transport_type, + username=self.configuration.netapp_login, + password=self.configuration.netapp_password, + hostname=self.configuration.netapp_server_hostname, + port=self.configuration.netapp_server_port, + vfiler=self.vfiler) + + self.vol_refresh_time = None + self.vol_refresh_interval = 1800 + self.vol_refresh_running = False + self.vol_refresh_voluntary = False + self.root_volume_name = self._get_root_volume_name() + + def check_for_setup_error(self): + """Check that the driver is working and can communicate.""" + api_version = self.zapi_client.get_ontapi_version() + if api_version: + major, minor = api_version + if major == 1 and minor < 9: + msg = _("Unsupported Data ONTAP version." + " Data ONTAP version 7.3.1 and above is supported.") + raise exception.VolumeBackendAPIException(data=msg) + else: + msg = _("API version could not be determined.") + raise exception.VolumeBackendAPIException(data=msg) + super(NetAppBlockStorage7modeLibrary, self).check_for_setup_error() + + def _create_lun(self, volume_name, lun_name, size, + metadata, qos_policy_group=None): + """Creates a LUN, handling Data ONTAP differences as needed.""" + + self.zapi_client.create_lun( + volume_name, lun_name, size, metadata, qos_policy_group) + + self.vol_refresh_voluntary = True + + def _get_root_volume_name(self): + # switch to volume-get-root-name API when possible + vols = self.zapi_client.get_filer_volumes() + for vol in vols: + volume_name = vol.get_child_content('name') + if self._get_vol_option(volume_name, 'root') == 'true': + return volume_name + LOG.warning(_LW('Could not determine root volume name ' + 'on %s.') % self._get_owner()) + return None + + def _get_owner(self): + if self.vfiler: + owner = '%s:%s' % (self.configuration.netapp_server_hostname, + self.vfiler) + else: + owner = self.configuration.netapp_server_hostname + return owner + + def _create_lun_handle(self, metadata): + """Returns LUN handle based on filer type.""" + owner = self._get_owner() + return '%s:%s' % (owner, metadata['Path']) + + def _find_mapped_lun_igroup(self, path, initiator, os=None): + """Find the igroup for mapped LUN with initiator.""" + igroup = None + lun_id = None + result = self.zapi_client.get_lun_map(path) + igroups = result.get_child_by_name('initiator-groups') + if igroups: + found = False + igroup_infs = igroups.get_children() + for ig in igroup_infs: + initiators = ig.get_child_by_name('initiators') + init_infs = initiators.get_children() + for info in init_infs: + if info.get_child_content('initiator-name') == initiator: + found = True + igroup = ig.get_child_content('initiator-group-name') + lun_id = ig.get_child_content('lun-id') + break + if found: + break + return igroup, lun_id + + def _clone_lun(self, name, new_name, space_reserved='true', + src_block=0, dest_block=0, block_count=0): + """Clone LUN with the given handle to the new name.""" + metadata = self._get_lun_attr(name, 'metadata') + path = metadata['Path'] + (parent, _splitter, name) = path.rpartition('/') + clone_path = '%s/%s' % (parent, new_name) + + self.zapi_client.clone_lun(path, clone_path, name, new_name, + space_reserved, src_block=0, + dest_block=0, block_count=0) + + self.vol_refresh_voluntary = True + luns = self.zapi_client.get_lun_by_args(path=clone_path) + cloned_lun = luns[0] + self.zapi_client.set_space_reserve(clone_path, space_reserved) + clone_meta = self._create_lun_meta(cloned_lun) + handle = self._create_lun_handle(clone_meta) + self._add_lun_to_table( + block_base.NetAppLun(handle, new_name, + cloned_lun.get_child_content('size'), + clone_meta)) + + def _create_lun_meta(self, lun): + """Creates LUN metadata dictionary.""" + self.zapi_client.check_is_naelement(lun) + meta_dict = {} + meta_dict['Path'] = lun.get_child_content('path') + meta_dict['Volume'] = lun.get_child_content('path').split('/')[2] + meta_dict['OsType'] = lun.get_child_content('multiprotocol-type') + meta_dict['SpaceReserved'] = lun.get_child_content( + 'is-space-reservation-enabled') + return meta_dict + + def _update_volume_stats(self): + """Retrieve stats info from filer.""" + + # ensure we get current data + self.vol_refresh_voluntary = True + self._refresh_volume_info() + + LOG.debug('Updating volume stats') + data = {} + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or self.driver_name + data['vendor_name'] = 'NetApp' + data['driver_version'] = self.VERSION + data['storage_protocol'] = self.driver_protocol + data['pools'] = self._get_pool_stats() + + self.zapi_client.provide_ems(self, self.driver_name, self.app_version, + server_type=self.driver_mode) + self._stats = data + + def _get_pool_stats(self): + """Retrieve pool (i.e. Data ONTAP volume) stats info from volumes.""" + + pools = [] + if not self.vols: + return pools + + for vol in self.vols: + + # omit volumes not specified in the config + volume_name = vol.get_child_content('name') + if self.volume_list and volume_name not in self.volume_list: + continue + + # omit root volume + if volume_name == self.root_volume_name: + continue + + # ensure good volume state + state = vol.get_child_content('state') + inconsistent = vol.get_child_content('is-inconsistent') + invalid = vol.get_child_content('is-invalid') + if (state != 'online' or + inconsistent != 'false' or + invalid != 'false'): + continue + + pool = dict() + pool['pool_name'] = volume_name + pool['QoS_support'] = False + pool['reserved_percentage'] = 0 + + # convert sizes to GB and de-rate by NetApp multiplier + total = float(vol.get_child_content('size-total') or 0) + total /= self.configuration.netapp_size_multiplier + total /= units.Gi + pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') + + free = float(vol.get_child_content('size-available') or 0) + free /= self.configuration.netapp_size_multiplier + free /= units.Gi + pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') + + pools.append(pool) + + return pools + + def _get_lun_block_count(self, path): + """Gets block counts for the LUN.""" + bs = super(NetAppBlockStorage7modeLibrary, + self)._get_lun_block_count(path) + api_version = self.zapi_client.get_ontapi_version() + if api_version: + major = api_version[0] + minor = api_version[1] + if major == 1 and minor < 15: + bs -= 1 + return bs + + def _refresh_volume_info(self): + """Saves the volume information for the filer.""" + + if (self.vol_refresh_time is None or self.vol_refresh_voluntary or + timeutils.is_newer_than(self.vol_refresh_time, + self.vol_refresh_interval)): + try: + job_set = na_utils.set_safe_attr(self, 'vol_refresh_running', + True) + if not job_set: + LOG.warning(_LW("Volume refresh job already running. " + "Returning...")) + return + self.vol_refresh_voluntary = False + self.vols = self.zapi_client.get_filer_volumes() + self.vol_refresh_time = timeutils.utcnow() + except Exception as e: + LOG.warning(_LW("Error refreshing volume info. Message: %s"), + six.text_type(e)) + finally: + na_utils.set_safe_attr(self, 'vol_refresh_running', False) + + def delete_volume(self, volume): + """Driver entry point for destroying existing volumes.""" + super(NetAppBlockStorage7modeLibrary, self).delete_volume(volume) + self.vol_refresh_voluntary = True diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py new file mode 100644 index 00000000000..5d2c7a2a7fb --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -0,0 +1,571 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Andrew Kerr. All rights reserved. +# Copyright (c) 2014 Jeff Applewhite. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver library for NetApp 7/C-mode block storage systems. +""" + +import sys +import uuid + +from oslo.utils import excutils +from oslo.utils import units +import six + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.openstack.common import log as logging +from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume import utils as volume_utils + + +LOG = logging.getLogger(__name__) + + +class NetAppLun(object): + """Represents a LUN on NetApp storage.""" + + def __init__(self, handle, name, size, metadata_dict): + self.handle = handle + self.name = name + self.size = size + self.metadata = metadata_dict or {} + + def get_metadata_property(self, prop): + """Get the metadata property of a LUN.""" + if prop in self.metadata: + return self.metadata[prop] + name = self.name + msg = _("No metadata property %(prop)s defined for the LUN %(name)s") + msg_fmt = {'prop': prop, 'name': name} + LOG.debug(msg % msg_fmt) + + def __str__(self, *args, **kwargs): + return 'NetApp Lun[handle:%s, name:%s, size:%s, metadata:%s]'\ + % (self.handle, self.name, self.size, self.metadata) + + +class NetAppBlockStorageLibrary(object): + """NetApp block storage library for Data ONTAP.""" + + # do not increment this as it may be used in volume type definitions + VERSION = "1.0.0" + IGROUP_PREFIX = 'openstack-' + REQUIRED_FLAGS = ['netapp_login', 'netapp_password', + 'netapp_server_hostname'] + + def __init__(self, driver_name, driver_protocol, **kwargs): + + na_utils.validate_instantiation(**kwargs) + + self.driver_name = driver_name + self.driver_protocol = driver_protocol + self.zapi_client = None + self._stats = {} + self.lun_table = {} + self.app_version = kwargs.get("app_version", "unknown") + + self.configuration = kwargs['configuration'] + self.configuration.append_config_values(na_opts.netapp_connection_opts) + self.configuration.append_config_values(na_opts.netapp_basicauth_opts) + self.configuration.append_config_values(na_opts.netapp_transport_opts) + self.configuration.append_config_values( + na_opts.netapp_provisioning_opts) + + def do_setup(self, context): + na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) + + def check_for_setup_error(self): + """Check that the driver is working and can communicate. + + Discovers the LUNs on the NetApp server. + """ + + lun_list = self.zapi_client.get_lun_list() + self._extract_and_populate_luns(lun_list) + LOG.debug("Success getting list of LUNs from server.") + + def get_pool(self, volume): + """Return pool name where volume resides. + + :param volume: The volume hosted by the driver. + :return: Name of the pool where given volume is hosted. + """ + name = volume['name'] + metadata = self._get_lun_attr(name, 'metadata') or dict() + return metadata.get('Volume', None) + + def create_volume(self, volume): + """Driver entry point for creating a new volume (Data ONTAP LUN).""" + + LOG.debug('create_volume on %s' % volume['host']) + + # get Data ONTAP volume name as pool name + ontap_volume_name = volume_utils.extract_host(volume['host'], + level='pool') + + if ontap_volume_name is None: + msg = _("Pool is not available in the volume host field.") + raise exception.InvalidHost(reason=msg) + + lun_name = volume['name'] + + # start with default size, get requested size + default_size = units.Mi * 100 # 100 MB + size = default_size if not int(volume['size'])\ + else int(volume['size']) * units.Gi + + metadata = {'OsType': 'linux', 'SpaceReserved': 'true'} + + extra_specs = na_utils.get_volume_extra_specs(volume) + qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ + if extra_specs else None + + # warn on obsolete extra specs + na_utils.log_extra_spec_warnings(extra_specs) + + self._create_lun(ontap_volume_name, lun_name, size, + metadata, qos_policy_group) + LOG.debug('Created LUN with name %s' % lun_name) + + metadata['Path'] = '/vol/%s/%s' % (ontap_volume_name, lun_name) + metadata['Volume'] = ontap_volume_name + metadata['Qtree'] = None + + handle = self._create_lun_handle(metadata) + self._add_lun_to_table(NetAppLun(handle, lun_name, size, metadata)) + + def delete_volume(self, volume): + """Driver entry point for destroying existing volumes.""" + name = volume['name'] + metadata = self._get_lun_attr(name, 'metadata') + if not metadata: + msg = _LW("No entry in LUN table for volume/snapshot %(name)s.") + msg_fmt = {'name': name} + LOG.warning(msg % msg_fmt) + return + self.zapi_client.destroy_lun(metadata['Path']) + self.lun_table.pop(name) + + def ensure_export(self, context, volume): + """Driver entry point to get the export info for an existing volume.""" + handle = self._get_lun_attr(volume['name'], 'handle') + return {'provider_location': handle} + + def create_export(self, context, volume): + """Driver entry point to get the export info for a new volume.""" + handle = self._get_lun_attr(volume['name'], 'handle') + return {'provider_location': handle} + + def remove_export(self, context, volume): + """Driver entry point to remove an export for a volume. + + Since exporting is idempotent in this driver, we have nothing + to do for unexporting. + """ + + pass + + def create_snapshot(self, snapshot): + """Driver entry point for creating a snapshot. + + This driver implements snapshots by using efficient single-file + (LUN) cloning. + """ + + vol_name = snapshot['volume_name'] + snapshot_name = snapshot['name'] + lun = self._get_lun_from_table(vol_name) + self._clone_lun(lun.name, snapshot_name, 'false') + + def delete_snapshot(self, snapshot): + """Driver entry point for deleting a snapshot.""" + self.delete_volume(snapshot) + LOG.debug("Snapshot %s deletion successful" % snapshot['name']) + + def create_volume_from_snapshot(self, volume, snapshot): + """Driver entry point for creating a new volume from a snapshot. + + Many would call this "cloning" and in fact we use cloning to implement + this feature. + """ + + vol_size = volume['size'] + snap_size = snapshot['volume_size'] + snapshot_name = snapshot['name'] + new_name = volume['name'] + self._clone_lun(snapshot_name, new_name, 'true') + if vol_size != snap_size: + try: + self.extend_volume(volume, volume['size']) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error( + _LE("Resizing %s failed. Cleaning volume."), new_name) + self.delete_volume(volume) + + def _create_lun(self, volume_name, lun_name, size, + metadata, qos_policy_group=None): + """Creates a LUN, handling Data ONTAP differences as needed.""" + raise NotImplementedError() + + def _create_lun_handle(self, metadata): + """Returns LUN handle based on filer type.""" + raise NotImplementedError() + + def _extract_and_populate_luns(self, api_luns): + """Extracts the LUNs from API. + + Populates in the LUN table. + """ + + for lun in api_luns: + meta_dict = self._create_lun_meta(lun) + path = lun.get_child_content('path') + (_rest, _splitter, name) = path.rpartition('/') + handle = self._create_lun_handle(meta_dict) + size = lun.get_child_content('size') + discovered_lun = NetAppLun(handle, name, size, meta_dict) + self._add_lun_to_table(discovered_lun) + + def _map_lun(self, name, initiator, initiator_type='iscsi', lun_id=None): + """Maps LUN to the initiator and returns LUN id assigned.""" + metadata = self._get_lun_attr(name, 'metadata') + os = metadata['OsType'] + path = metadata['Path'] + if self._check_allowed_os(os): + os = os + else: + os = 'default' + igroup_name = self._get_or_create_igroup(initiator, + initiator_type, os) + try: + return self.zapi_client.map_lun(path, igroup_name, lun_id=lun_id) + except NaApiError: + exc_info = sys.exc_info() + (_igroup, lun_id) = self._find_mapped_lun_igroup(path, initiator) + if lun_id is not None: + return lun_id + else: + raise exc_info[0], exc_info[1], exc_info[2] + + def _unmap_lun(self, path, initiator): + """Unmaps a LUN from given initiator.""" + (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path, initiator) + self.zapi_client.unmap_lun(path, igroup_name) + + def _find_mapped_lun_igroup(self, path, initiator, os=None): + """Find the igroup for mapped LUN with initiator.""" + raise NotImplementedError() + + def _get_or_create_igroup(self, initiator, initiator_type='iscsi', + os='default'): + """Checks for an igroup for an initiator. + + Creates igroup if not found. + """ + + igroups = self.zapi_client.get_igroup_by_initiator(initiator=initiator) + igroup_name = None + for igroup in igroups: + if igroup['initiator-group-os-type'] == os: + if igroup['initiator-group-type'] == initiator_type or \ + igroup['initiator-group-type'] == 'mixed': + if igroup['initiator-group-name'].startswith( + self.IGROUP_PREFIX): + igroup_name = igroup['initiator-group-name'] + break + if not igroup_name: + igroup_name = self.IGROUP_PREFIX + six.text_type(uuid.uuid4()) + self.zapi_client.create_igroup(igroup_name, initiator_type, os) + self.zapi_client.add_igroup_initiator(igroup_name, initiator) + return igroup_name + + def _check_allowed_os(self, os): + """Checks if the os type supplied is NetApp supported.""" + if os in ['linux', 'aix', 'hpux', 'windows', 'solaris', + 'netware', 'vmware', 'openvms', 'xen', 'hyper_v']: + return True + else: + return False + + def _add_lun_to_table(self, lun): + """Adds LUN to cache table.""" + if not isinstance(lun, NetAppLun): + msg = _("Object is not a NetApp LUN.") + raise exception.VolumeBackendAPIException(data=msg) + self.lun_table[lun.name] = lun + + def _get_lun_from_table(self, name): + """Gets LUN from cache table. + + Refreshes cache if LUN not found in cache. + """ + lun = self.lun_table.get(name) + if lun is None: + lun_list = self.zapi_client.get_lun_list() + self._extract_and_populate_luns(lun_list) + lun = self.lun_table.get(name) + if lun is None: + raise exception.VolumeNotFound(volume_id=name) + return lun + + def _clone_lun(self, name, new_name, space_reserved='true', + src_block=0, dest_block=0, block_count=0): + """Clone LUN with the given name to the new name.""" + raise NotImplementedError() + + def _get_lun_attr(self, name, attr): + """Get the LUN attribute if found else None.""" + try: + attr = getattr(self._get_lun_from_table(name), attr) + return attr + except exception.VolumeNotFound as e: + LOG.error(_LE("Message: %s"), e.msg) + except Exception as e: + LOG.error(_LE("Error getting LUN attribute. Exception: %s"), + e.__str__()) + return None + + def _create_lun_meta(self, lun): + raise NotImplementedError() + + def create_cloned_volume(self, volume, src_vref): + """Creates a clone of the specified volume.""" + vol_size = volume['size'] + src_vol = self._get_lun_from_table(src_vref['name']) + src_vol_size = src_vref['size'] + new_name = volume['name'] + self._clone_lun(src_vol.name, new_name, 'true') + if vol_size != src_vol_size: + try: + self.extend_volume(volume, volume['size']) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error( + _LE("Resizing %s failed. Cleaning volume."), new_name) + self.delete_volume(volume) + + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If 'refresh' is True, run update the stats first. + """ + + if refresh: + self._update_volume_stats() + + return self._stats + + def _update_volume_stats(self): + raise NotImplementedError() + + def extend_volume(self, volume, new_size): + """Extend an existing volume to the new size.""" + name = volume['name'] + lun = self._get_lun_from_table(name) + path = lun.metadata['Path'] + curr_size_bytes = six.text_type(lun.size) + new_size_bytes = six.text_type(int(new_size) * units.Gi) + # Reused by clone scenarios. + # Hence comparing the stored size. + if curr_size_bytes != new_size_bytes: + lun_geometry = self.zapi_client.get_lun_geometry(path) + if (lun_geometry and lun_geometry.get("max_resize") + and int(lun_geometry.get("max_resize")) >= + int(new_size_bytes)): + self.zapi_client.do_direct_resize(path, new_size_bytes) + else: + self._do_sub_clone_resize(path, new_size_bytes) + self.lun_table[name].size = new_size_bytes + else: + LOG.info(_LI("No need to extend volume %s" + " as it is already the requested new size."), name) + + def _get_vol_option(self, volume_name, option_name): + """Get the value for the volume option.""" + value = None + options = self.zapi_client.get_volume_options(volume_name) + for opt in options: + if opt.get_child_content('name') == option_name: + value = opt.get_child_content('value') + break + return value + + def _do_sub_clone_resize(self, path, new_size_bytes): + """Does sub LUN clone after verification. + + Clones the block ranges and swaps + the LUNs also deletes older LUN + after a successful clone. + """ + seg = path.split("/") + LOG.info(_LI("Resizing LUN %s to new size using clone operation."), + seg[-1]) + name = seg[-1] + vol_name = seg[2] + lun = self._get_lun_from_table(name) + metadata = lun.metadata + compression = self._get_vol_option(vol_name, 'compression') + if compression == "on": + msg = _('%s cannot be resized using clone operation' + ' as it is hosted on compressed volume') + raise exception.VolumeBackendAPIException(data=msg % name) + else: + block_count = self._get_lun_block_count(path) + if block_count == 0: + msg = _('%s cannot be resized using clone operation' + ' as it contains no blocks.') + raise exception.VolumeBackendAPIException(data=msg % name) + new_lun = 'new-%s' % name + self.zapi_client.create_lun(vol_name, new_lun, new_size_bytes, + metadata) + try: + self._clone_lun(name, new_lun, block_count=block_count) + self._post_sub_clone_resize(path) + except Exception: + with excutils.save_and_reraise_exception(): + new_path = '/vol/%s/%s' % (vol_name, new_lun) + self.zapi_client.destroy_lun(new_path) + + def _post_sub_clone_resize(self, path): + """Try post sub clone resize in a transactional manner.""" + st_tm_mv, st_nw_mv, st_del_old = None, None, None + seg = path.split("/") + LOG.info(_LI("Post clone resize LUN %s"), seg[-1]) + new_lun = 'new-%s' % (seg[-1]) + tmp_lun = 'tmp-%s' % (seg[-1]) + tmp_path = "/vol/%s/%s" % (seg[2], tmp_lun) + new_path = "/vol/%s/%s" % (seg[2], new_lun) + try: + st_tm_mv = self.zapi_client.move_lun(path, tmp_path) + st_nw_mv = self.zapi_client.move_lun(new_path, path) + st_del_old = self.zapi_client.destroy_lun(tmp_path) + except Exception as e: + if st_tm_mv is None: + msg = _("Failure staging LUN %s to tmp.") + raise exception.VolumeBackendAPIException(data=msg % (seg[-1])) + else: + if st_nw_mv is None: + self.zapi_client.move_lun(tmp_path, path) + msg = _("Failure moving new cloned LUN to %s.") + raise exception.VolumeBackendAPIException( + data=msg % (seg[-1])) + elif st_del_old is None: + LOG.error(_LE("Failure deleting staged tmp LUN %s."), + tmp_lun) + else: + LOG.error(_LE("Unknown exception in" + " post clone resize LUN %s."), seg[-1]) + LOG.error(_LE("Exception details: %s") % (e.__str__())) + + def _get_lun_block_count(self, path): + """Gets block counts for the LUN.""" + LOG.debug("Getting LUN block count.") + lun_infos = self.zapi_client.get_lun_by_args(path=path) + if not lun_infos: + seg = path.split('/') + msg = _('Failure getting LUN info for %s.') + raise exception.VolumeBackendAPIException(data=msg % seg[-1]) + lun_info = lun_infos[-1] + bs = int(lun_info.get_child_content('block-size')) + ls = int(lun_info.get_child_content('size')) + block_count = ls / bs + return block_count + + def initialize_connection_iscsi(self, volume, connector): + """Driver entry point to attach a volume to an instance. + + Do the LUN masking on the storage system so the initiator can access + the LUN on the target. Also return the iSCSI properties so the + initiator can find the LUN. This implementation does not call + _get_iscsi_properties() to get the properties because cannot store the + LUN number in the database. We only find out what the LUN number will + be during this method call so we construct the properties dictionary + ourselves. + """ + + initiator_name = connector['initiator'] + name = volume['name'] + lun_id = self._map_lun(name, initiator_name, 'iscsi', None) + msg = _("Mapped LUN %(name)s to the initiator %(initiator_name)s") + msg_fmt = {'name': name, 'initiator_name': initiator_name} + LOG.debug(msg % msg_fmt) + iqn = self.zapi_client.get_iscsi_service_details() + target_details_list = self.zapi_client.get_target_details() + msg = _("Successfully fetched target details for LUN %(name)s and " + "initiator %(initiator_name)s") + msg_fmt = {'name': name, 'initiator_name': initiator_name} + LOG.debug(msg % msg_fmt) + + if not target_details_list: + msg = _('Failed to get LUN target details for the LUN %s') + raise exception.VolumeBackendAPIException(data=msg % name) + target_details = None + for tgt_detail in target_details_list: + if tgt_detail.get('interface-enabled', 'true') == 'true': + target_details = tgt_detail + break + if not target_details: + target_details = target_details_list[0] + + if not target_details['address'] and target_details['port']: + msg = _('Failed to get target portal for the LUN %s') + raise exception.VolumeBackendAPIException(data=msg % name) + if not iqn: + msg = _('Failed to get target IQN for the LUN %s') + raise exception.VolumeBackendAPIException(data=msg % name) + + properties = {} + properties['target_discovered'] = False + (address, port) = (target_details['address'], target_details['port']) + properties['target_portal'] = '%s:%s' % (address, port) + properties['target_iqn'] = iqn + properties['target_lun'] = lun_id + properties['volume_id'] = volume['id'] + + auth = volume['provider_auth'] + if auth: + (auth_method, auth_username, auth_secret) = auth.split() + properties['auth_method'] = auth_method + properties['auth_username'] = auth_username + properties['auth_password'] = auth_secret + + return { + 'driver_volume_type': 'iscsi', + 'data': properties, + } + + def terminate_connection_iscsi(self, volume, connector, **kwargs): + """Driver entry point to unattach a volume from an instance. + + Unmask the LUN on the storage system so the given initiator can no + longer access it. + """ + + initiator_name = connector['initiator'] + name = volume['name'] + metadata = self._get_lun_attr(name, 'metadata') + path = metadata['Path'] + self._unmap_lun(path, initiator_name) + msg = _("Unmapped LUN %(name)s from the initiator %(initiator_name)s") + msg_fmt = {'name': name, 'initiator_name': initiator_name} + LOG.debug(msg % msg_fmt) diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py new file mode 100644 index 00000000000..774b4c0a753 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -0,0 +1,241 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Andrew Kerr. All rights reserved. +# Copyright (c) 2014 Jeff Applewhite. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver library for NetApp C-mode block storage systems. +""" + +import copy + +from oslo.utils import units +import six + +from cinder import exception +from cinder.i18n import _ +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp.dataontap.client import client_cmode +from cinder.volume.drivers.netapp.dataontap import ssc_cmode +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils + + +LOG = logging.getLogger(__name__) + + +class NetAppBlockStorageCmodeLibrary(block_base. + NetAppBlockStorageLibrary): + """NetApp block storage library for Data ONTAP (Cluster-mode).""" + + REQUIRED_CMODE_FLAGS = ['netapp_vserver'] + + def __init__(self, driver_name, driver_protocol, **kwargs): + super(NetAppBlockStorageCmodeLibrary, self).__init__(driver_name, + driver_protocol, + **kwargs) + self.configuration.append_config_values(na_opts.netapp_cluster_opts) + self.driver_mode = 'cluster' + + def do_setup(self, context): + super(NetAppBlockStorageCmodeLibrary, self).do_setup(context) + na_utils.check_flags(self.REQUIRED_CMODE_FLAGS, self.configuration) + + self.vserver = self.configuration.netapp_vserver + + self.zapi_client = client_cmode.Client( + transport_type=self.configuration.netapp_transport_type, + username=self.configuration.netapp_login, + password=self.configuration.netapp_password, + hostname=self.configuration.netapp_server_hostname, + port=self.configuration.netapp_server_port, + vserver=self.vserver) + + self.ssc_vols = None + self.stale_vols = set() + + def check_for_setup_error(self): + """Check that the driver is working and can communicate.""" + ssc_cmode.check_ssc_api_permissions(self.zapi_client) + super(NetAppBlockStorageCmodeLibrary, self).check_for_setup_error() + + def _create_lun(self, volume_name, lun_name, size, + metadata, qos_policy_group=None): + """Creates a LUN, handling Data ONTAP differences as needed.""" + + self.zapi_client.create_lun( + volume_name, lun_name, size, metadata, qos_policy_group) + + self._update_stale_vols( + volume=ssc_cmode.NetAppVolume(volume_name, self.vserver)) + + def _create_lun_handle(self, metadata): + """Returns LUN handle based on filer type.""" + return '%s:%s' % (self.vserver, metadata['Path']) + + def _find_mapped_lun_igroup(self, path, initiator, os=None): + """Find the igroup for mapped LUN with initiator.""" + initiator_igroups = self.zapi_client.get_igroup_by_initiator( + initiator=initiator) + lun_maps = self.zapi_client.get_lun_map(path) + if initiator_igroups and lun_maps: + for igroup in initiator_igroups: + igroup_name = igroup['initiator-group-name'] + if igroup_name.startswith(self.IGROUP_PREFIX): + for lun_map in lun_maps: + if lun_map['initiator-group'] == igroup_name: + return igroup_name, lun_map['lun-id'] + return None, None + + def _clone_lun(self, name, new_name, space_reserved='true', + src_block=0, dest_block=0, block_count=0): + """Clone LUN with the given handle to the new name.""" + metadata = self._get_lun_attr(name, 'metadata') + volume = metadata['Volume'] + self.zapi_client.clone_lun(volume, name, new_name, space_reserved, + src_block=0, dest_block=0, block_count=0) + LOG.debug("Cloned LUN with new name %s" % new_name) + lun = self.zapi_client.get_lun_by_args(vserver=self.vserver, + path='/vol/%s/%s' + % (volume, new_name)) + if len(lun) == 0: + msg = _("No cloned LUN named %s found on the filer") + raise exception.VolumeBackendAPIException(data=msg % new_name) + clone_meta = self._create_lun_meta(lun[0]) + self._add_lun_to_table( + block_base.NetAppLun('%s:%s' % (clone_meta['Vserver'], + clone_meta['Path']), + new_name, + lun[0].get_child_content('size'), + clone_meta)) + self._update_stale_vols( + volume=ssc_cmode.NetAppVolume(volume, self.vserver)) + + def _create_lun_meta(self, lun): + """Creates LUN metadata dictionary.""" + self.zapi_client.check_is_naelement(lun) + meta_dict = {} + meta_dict['Vserver'] = lun.get_child_content('vserver') + meta_dict['Volume'] = lun.get_child_content('volume') + meta_dict['Qtree'] = lun.get_child_content('qtree') + meta_dict['Path'] = lun.get_child_content('path') + meta_dict['OsType'] = lun.get_child_content('multiprotocol-type') + meta_dict['SpaceReserved'] = \ + lun.get_child_content('is-space-reservation-enabled') + return meta_dict + + def _configure_tunneling(self, do_tunneling=False): + """Configures tunneling for Data ONTAP cluster.""" + if do_tunneling: + self.zapi_client.set_vserver(self.vserver) + else: + self.zapi_client.set_vserver(None) + + def _update_volume_stats(self): + """Retrieve stats info from vserver.""" + + sync = True if self.ssc_vols is None else False + ssc_cmode.refresh_cluster_ssc(self, self.zapi_client.get_connection(), + self.vserver, synchronous=sync) + + LOG.debug('Updating volume stats') + data = {} + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or self.driver_name + data['vendor_name'] = 'NetApp' + data['driver_version'] = self.VERSION + data['storage_protocol'] = self.driver_protocol + data['pools'] = self._get_pool_stats() + + self.zapi_client.provide_ems(self, self.driver_name, self.app_version) + self._stats = data + + def _get_pool_stats(self): + """Retrieve pool (Data ONTAP volume) stats info from SSC volumes.""" + + pools = [] + if not self.ssc_vols: + return pools + + for vol in self.ssc_vols['all']: + pool = dict() + pool['pool_name'] = vol.id['name'] + pool['QoS_support'] = False + pool['reserved_percentage'] = 0 + + # convert sizes to GB and de-rate by NetApp multiplier + total = float(vol.space['size_total_bytes']) + total /= self.configuration.netapp_size_multiplier + total /= units.Gi + pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') + + free = float(vol.space['size_avl_bytes']) + free /= self.configuration.netapp_size_multiplier + free /= units.Gi + pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') + + pool['netapp_raid_type'] = vol.aggr['raid_type'] + pool['netapp_disk_type'] = vol.aggr['disk_type'] + + mirrored = vol in self.ssc_vols['mirrored'] + pool['netapp_mirrored'] = six.text_type(mirrored).lower() + pool['netapp_unmirrored'] = six.text_type(not mirrored).lower() + + dedup = vol in self.ssc_vols['dedup'] + pool['netapp_dedup'] = six.text_type(dedup).lower() + pool['netapp_nodedup'] = six.text_type(not dedup).lower() + + compression = vol in self.ssc_vols['compression'] + pool['netapp_compression'] = six.text_type(compression).lower() + pool['netapp_nocompression'] = six.text_type( + not compression).lower() + + thin = vol in self.ssc_vols['thin'] + pool['netapp_thin_provisioned'] = six.text_type(thin).lower() + pool['netapp_thick_provisioned'] = six.text_type(not thin).lower() + + pools.append(pool) + + return pools + + @utils.synchronized('update_stale') + def _update_stale_vols(self, volume=None, reset=False): + """Populates stale vols with vol and returns set copy if reset.""" + if volume: + self.stale_vols.add(volume) + if reset: + set_copy = copy.deepcopy(self.stale_vols) + self.stale_vols.clear() + return set_copy + + @utils.synchronized("refresh_ssc_vols") + def refresh_ssc_vols(self, vols): + """Refreshes ssc_vols with latest entries.""" + self.ssc_vols = vols + + def delete_volume(self, volume): + """Driver entry point for destroying existing volumes.""" + lun = self.lun_table.get(volume['name']) + netapp_vol = None + if lun: + netapp_vol = lun.get_metadata_property('Volume') + super(NetAppBlockStorageCmodeLibrary, self).delete_volume(volume) + if netapp_vol: + self._update_stale_vols( + volume=ssc_cmode.NetAppVolume(netapp_vol, self.vserver)) diff --git a/cinder/volume/drivers/netapp/dataontap/client/__init__.py b/cinder/volume/drivers/netapp/dataontap/client/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/netapp/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py similarity index 82% rename from cinder/volume/drivers/netapp/api.py rename to cinder/volume/drivers/netapp/dataontap/client/api.py index 3c419e55191..92a720d88d8 100644 --- a/cinder/volume/drivers/netapp/api.py +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -1,6 +1,7 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# All Rights Reserved. +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Glenn Gobeli. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -14,16 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. """ -NetApp api for ONTAP and OnCommand DFM. +NetApp API for Data ONTAP and OnCommand DFM. -Contains classes required to issue api calls to ONTAP and OnCommand DFM. +Contains classes required to issue API calls to Data ONTAP and OnCommand DFM. """ +import copy import urllib2 from lxml import etree import six +from cinder import exception from cinder.i18n import _ from cinder.openstack.common import log as logging @@ -48,21 +51,25 @@ class NaServer(object): def __init__(self, host, server_type=SERVER_TYPE_FILER, transport_type=TRANSPORT_TYPE_HTTP, style=STYLE_LOGIN_PASSWORD, username=None, - password=None): + password=None, port=None): self._host = host self.set_server_type(server_type) self.set_transport_type(transport_type) self.set_style(style) + if port: + self.set_port(port) self._username = username self._password = password self._refresh_conn = True + LOG.debug('Using NetApp controller: %s' % self._host) + def get_transport_type(self): """Get the transport type protocol.""" return self._protocol def set_transport_type(self, transport_type): - """Set the transport type protocol for api. + """Set the transport type protocol for API. Supports http and https transport types. """ @@ -118,7 +125,7 @@ class NaServer(object): self._refresh_conn = True def set_api_version(self, major, minor): - """Set the api version.""" + """Set the API version.""" try: self._api_major_version = int(major) self._api_minor_version = int(minor) @@ -129,7 +136,7 @@ class NaServer(object): self._refresh_conn = True def get_api_version(self): - """Gets the api version tuple.""" + """Gets the API version tuple.""" if hasattr(self, '_api_version'): return (self._api_major_version, self._api_minor_version) return None @@ -187,9 +194,9 @@ class NaServer(object): self._refresh_conn = True def invoke_elem(self, na_element, enable_tunneling=False): - """Invoke the api on the server.""" + """Invoke the API on the server.""" if na_element and not isinstance(na_element, NaElement): - ValueError('NaElement must be supplied to invoke api') + ValueError('NaElement must be supplied to invoke API') request = self._create_request(na_element, enable_tunneling) if not hasattr(self, '_opener') or not self._opener \ or self._refresh_conn: @@ -207,7 +214,7 @@ class NaServer(object): return self._get_result(xml) def invoke_successfully(self, na_element, enable_tunneling=False): - """Invokes api and checks execution status as success. + """Invokes API and checks execution status as success. Need to set enable_tunneling to True explicitly to achieve it. This helps to use same connection instance to enable or disable @@ -303,7 +310,7 @@ class NaServer(object): class NaElement(object): - """Class wraps basic building block for NetApp api request.""" + """Class wraps basic building block for NetApp API request.""" def __init__(self, name): """Name of the element or etree.Element.""" @@ -496,16 +503,96 @@ class NaElement(object): class NaApiError(Exception): - """Base exception class for NetApp api errors.""" + """Base exception class for NetApp API errors.""" def __init__(self, code='unknown', message='unknown'): self.code = code self.message = message def __str__(self, *args, **kwargs): - return 'NetApp api failed. Reason - %s:%s' % (self.code, self.message) + return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message) NaErrors = {'API_NOT_FOUND': NaApiError('13005', 'Unable to find API'), 'INSUFFICIENT_PRIVS': NaApiError('13003', 'Insufficient privileges')} + + +def invoke_api(na_server, api_name, api_family='cm', query=None, + des_result=None, additional_elems=None, + is_iter=False, records=0, tag=None, + timeout=0, tunnel=None): + """Invokes any given API call to a NetApp server. + + :param na_server: na_server instance + :param api_name: API name string + :param api_family: cm or 7m + :param query: API query as dict + :param des_result: desired result as dict + :param additional_elems: dict other than query and des_result + :param is_iter: is iterator API + :param records: limit for records, 0 for infinite + :param timeout: timeout seconds + :param tunnel: tunnel entity, vserver or vfiler name + """ + record_step = 50 + if not (na_server or isinstance(na_server, NaServer)): + msg = _("Requires an NaServer instance.") + raise exception.InvalidInput(reason=msg) + server = copy.copy(na_server) + if api_family == 'cm': + server.set_vserver(tunnel) + else: + server.set_vfiler(tunnel) + if timeout > 0: + server.set_timeout(timeout) + iter_records = 0 + cond = True + while cond: + na_element = create_api_request( + api_name, query, des_result, additional_elems, + is_iter, record_step, tag) + result = server.invoke_successfully(na_element, True) + if is_iter: + if records > 0: + iter_records = iter_records + record_step + if iter_records >= records: + cond = False + tag_el = result.get_child_by_name('next-tag') + tag = tag_el.get_content() if tag_el else None + if not tag: + cond = False + else: + cond = False + yield result + + +def create_api_request(api_name, query=None, des_result=None, + additional_elems=None, is_iter=False, + record_step=50, tag=None): + """Creates a NetApp API request. + + :param api_name: API name string + :param query: API query as dict + :param des_result: desired result as dict + :param additional_elems: dict other than query and des_result + :param is_iter: is iterator API + :param record_step: records at a time for iter API + :param tag: next tag for iter API + """ + api_el = NaElement(api_name) + if query: + query_el = NaElement('query') + query_el.translate_struct(query) + api_el.add_child_elem(query_el) + if des_result: + res_el = NaElement('desired-attributes') + res_el.translate_struct(des_result) + api_el.add_child_elem(res_el) + if additional_elems: + api_el.translate_struct(additional_elems) + if is_iter: + api_el.add_new_child('max-records', six.text_type(record_step)) + if tag: + api_el.add_new_child('tag', tag, True) + return api_el diff --git a/cinder/volume/drivers/netapp/client/seven_mode.py b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py similarity index 93% rename from cinder/volume/drivers/netapp/client/seven_mode.py rename to cinder/volume/drivers/netapp/dataontap/client/client_7mode.py index 2ee84078c86..17c14b44a3c 100644 --- a/cinder/volume/drivers/netapp/client/seven_mode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py @@ -1,5 +1,5 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# All Rights Reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -23,17 +23,23 @@ import six from cinder import exception from cinder.i18n import _, _LW from cinder.openstack.common import log as logging -from cinder.volume.drivers.netapp import api as netapp_api -from cinder.volume.drivers.netapp.client import base +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_base LOG = logging.getLogger(__name__) -class Client(base.Client): +class Client(client_base.Client): + + def __init__(self, volume_list=None, **kwargs): + super(Client, self).__init__(**kwargs) + vfiler = kwargs.get('vfiler', None) + self.connection.set_vfiler(vfiler) + + (major, minor) = self.get_ontapi_version(cached=False) + self.connection.set_api_version(major, minor) - def __init__(self, connection, volume_list=None): - super(Client, self).__init__(connection) self.volume_list = volume_list def _invoke_vfiler_api(self, na_element, vfiler): @@ -66,7 +72,7 @@ class Client(base.Client): return result.get_child_content('node-name') def get_lun_list(self): - """Gets the list of luns on filer.""" + """Gets the list of LUNs on filer.""" lun_list = [] if self.volume_list: for vol in self.volume_list: @@ -75,15 +81,15 @@ class Client(base.Client): if luns: lun_list.extend(luns) except netapp_api.NaApiError: - LOG.warning(_LW("Error finding luns for volume %s." - " Verify volume exists.") % (vol)) + LOG.warning(_LW("Error finding LUNs for volume %s." + " Verify volume exists.") % vol) else: luns = self._get_vol_luns(None) lun_list.extend(luns) return lun_list def _get_vol_luns(self, vol_name): - """Gets the luns for a volume.""" + """Gets the LUNs for a volume.""" api = netapp_api.NaElement('lun-list-info') if vol_name: api.add_new_child('volume-name', vol_name) @@ -132,7 +138,7 @@ class Client(base.Client): zbc = block_count if z_calls == 0: z_calls = 1 - for call in range(0, z_calls): + for _call in range(0, z_calls): if zbc > z_limit: block_count = z_limit zbc -= z_limit @@ -148,7 +154,7 @@ class Client(base.Client): bc_limit = 2 ** 24 # 8GB segments = int(math.ceil(block_count / float(bc_limit))) bc = block_count - for segment in range(0, segments): + for _segment in range(0, segments): if bc > bc_limit: block_count = bc_limit bc -= bc_limit @@ -213,7 +219,7 @@ class Client(base.Client): clone_ops_info.get_child_content('reason')) def get_lun_by_args(self, **args): - """Retrieves luns with specified args.""" + """Retrieves LUNs with specified args.""" lun_info = netapp_api.NaElement.create_node_with_children( 'lun-list-info', **args) result = self.connection.invoke_successfully(lun_info, True) @@ -221,7 +227,7 @@ class Client(base.Client): return luns.get_children() def get_filer_volumes(self, volume=None): - """Returns list of filer volumes in api format.""" + """Returns list of filer volumes in API format.""" vol_request = netapp_api.NaElement('volume-list-info') res = self.connection.invoke_successfully(vol_request, True) volumes = res.get_child_by_name('volumes') diff --git a/cinder/volume/drivers/netapp/client/base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py similarity index 55% rename from cinder/volume/drivers/netapp/client/base.py rename to cinder/volume/drivers/netapp/dataontap/client/client_base.py index 4595dbd353b..965bed865fa 100644 --- a/cinder/volume/drivers/netapp/client/base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -1,5 +1,5 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# All Rights Reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -13,14 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import socket import sys from oslo.utils import excutils +from oslo.utils import timeutils import six from cinder.i18n import _LE, _LW, _LI from cinder.openstack.common import log as logging -from cinder.volume.drivers.netapp import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError +from cinder.volume.drivers.netapp.dataontap.client.api import NaElement +from cinder.volume.drivers.netapp.dataontap.client.api import NaServer LOG = logging.getLogger(__name__) @@ -28,16 +34,32 @@ LOG = logging.getLogger(__name__) class Client(object): - def __init__(self, connection): - self.connection = connection + def __init__(self, **kwargs): + self.connection = NaServer(host=kwargs['hostname'], + transport_type=kwargs['transport_type'], + port=kwargs['port'], + username=kwargs['username'], + password=kwargs['password']) - def get_ontapi_version(self): + def get_ontapi_version(self, cached=True): """Gets the supported ontapi version.""" + + if cached: + return self.connection.get_api_version() + ontapi_version = netapp_api.NaElement('system-get-ontapi-version') res = self.connection.invoke_successfully(ontapi_version, False) major = res.get_child_content('major-version') minor = res.get_child_content('minor-version') - return (major, minor) + return major, minor + + def get_connection(self): + return self.connection + + def check_is_naelement(self, elem): + """Checks if object is instance of NaElement.""" + if not isinstance(elem, NaElement): + raise ValueError('Expects NaElement') def create_lun(self, volume_name, lun_name, size, metadata, qos_policy_group=None): @@ -64,7 +86,7 @@ class Client(object): LOG.error(msg % msg_args) def destroy_lun(self, path, force=True): - """Destroys the lun at the path.""" + """Destroys the LUN at the path.""" lun_destroy = netapp_api.NaElement.create_node_with_children( 'lun-destroy', **{'path': path}) @@ -75,7 +97,7 @@ class Client(object): LOG.debug("Destroyed LUN %s" % seg[-1]) def map_lun(self, path, igroup_name, lun_id=None): - """Maps lun to the initiator and returns lun id assigned.""" + """Maps LUN to the initiator and returns LUN id assigned.""" lun_map = netapp_api.NaElement.create_node_with_children( 'lun-map', **{'path': path, 'initiator-group': igroup_name}) @@ -87,25 +109,25 @@ class Client(object): except netapp_api.NaApiError as e: code = e.code message = e.message - msg = _LW('Error mapping lun. Code :%(code)s, Message:%(message)s') + msg = _LW('Error mapping LUN. Code :%(code)s, Message:%(message)s') msg_fmt = {'code': code, 'message': message} LOG.warning(msg % msg_fmt) raise def unmap_lun(self, path, igroup_name): - """Unmaps a lun from given initiator.""" + """Unmaps a LUN from given initiator.""" lun_unmap = netapp_api.NaElement.create_node_with_children( 'lun-unmap', **{'path': path, 'initiator-group': igroup_name}) try: self.connection.invoke_successfully(lun_unmap, True) except netapp_api.NaApiError as e: - msg = _LW("Error unmapping lun. Code :%(code)s," + msg = _LW("Error unmapping LUN. Code :%(code)s," " Message:%(message)s") msg_fmt = {'code': e.code, 'message': e.message} exc_info = sys.exc_info() LOG.warning(msg % msg_fmt) - # if the lun is already unmapped + # if the LUN is already unmapped if e.code == '13115' or e.code == '9016': pass else: @@ -129,9 +151,9 @@ class Client(object): self.connection.invoke_successfully(igroup_add, True) def do_direct_resize(self, path, new_size_bytes, force=True): - """Resize the lun.""" + """Resize the LUN.""" seg = path.split("/") - LOG.info(_LI("Resizing lun %s directly to new size."), seg[-1]) + LOG.info(_LI("Resizing LUN %s directly to new size."), seg[-1]) lun_resize = netapp_api.NaElement.create_node_with_children( 'lun-resize', **{'path': path, @@ -141,7 +163,7 @@ class Client(object): self.connection.invoke_successfully(lun_resize, True) def get_lun_geometry(self, path): - """Gets the lun geometry.""" + """Gets the LUN geometry.""" geometry = {} lun_geo = netapp_api.NaElement("lun-get-geometry") lun_geo.add_new_child('path', path) @@ -159,7 +181,7 @@ class Client(object): geometry['max_resize'] =\ result.get_child_content("max-resize-size") except Exception as e: - LOG.error(_LE("Lun %(path)s geometry failed. Message - %(msg)s") + LOG.error(_LE("LUN %(path)s geometry failed. Message - %(msg)s") % {'path': path, 'msg': e.message}) return geometry @@ -175,10 +197,10 @@ class Client(object): return opts def move_lun(self, path, new_path): - """Moves the lun at path to new path.""" + """Moves the LUN at path to new path.""" seg = path.split("/") new_seg = new_path.split("/") - LOG.debug("Moving lun %(name)s to %(new_name)s." + LOG.debug("Moving LUN %(name)s to %(new_name)s." % {'name': seg[-1], 'new_name': new_seg[-1]}) lun_move = netapp_api.NaElement("lun-move") lun_move.add_new_child("path", path) @@ -194,7 +216,7 @@ class Client(object): raise NotImplementedError() def get_lun_list(self): - """Gets the list of luns on filer.""" + """Gets the list of LUNs on filer.""" raise NotImplementedError() def get_igroup_by_initiator(self, initiator): @@ -202,5 +224,92 @@ class Client(object): raise NotImplementedError() def get_lun_by_args(self, **args): - """Retrieves luns with specified args.""" + """Retrieves LUNs with specified args.""" raise NotImplementedError() + + def provide_ems(self, requester, netapp_backend, app_version, + server_type="cluster"): + """Provide ems with volume stats for the requester. + + :param server_type: cluster or 7mode. + """ + def _create_ems(netapp_backend, app_version, server_type): + """Create ems API request.""" + ems_log = NaElement('ems-autosupport-log') + host = socket.getfqdn() or 'Cinder_node' + if server_type == "cluster": + dest = "cluster node" + else: + dest = "7 mode controller" + ems_log.add_new_child('computer-name', host) + ems_log.add_new_child('event-id', '0') + ems_log.add_new_child('event-source', + 'Cinder driver %s' % netapp_backend) + ems_log.add_new_child('app-version', app_version) + ems_log.add_new_child('category', 'provisioning') + ems_log.add_new_child('event-description', + 'OpenStack Cinder connected to %s' % dest) + ems_log.add_new_child('log-level', '6') + ems_log.add_new_child('auto-support', 'false') + return ems_log + + def _create_vs_get(): + """Create vs_get API request.""" + vs_get = NaElement('vserver-get-iter') + vs_get.add_new_child('max-records', '1') + query = NaElement('query') + query.add_node_with_children('vserver-info', + **{'vserver-type': 'node'}) + vs_get.add_child_elem(query) + desired = NaElement('desired-attributes') + desired.add_node_with_children( + 'vserver-info', **{'vserver-name': '', 'vserver-type': ''}) + vs_get.add_child_elem(desired) + return vs_get + + def _get_cluster_node(na_server): + """Get the cluster node for ems.""" + na_server.set_vserver(None) + vs_get = _create_vs_get() + res = na_server.invoke_successfully(vs_get) + if (res.get_child_content('num-records') and + int(res.get_child_content('num-records')) > 0): + attr_list = res.get_child_by_name('attributes-list') + vs_info = attr_list.get_child_by_name('vserver-info') + vs_name = vs_info.get_child_content('vserver-name') + return vs_name + return None + + do_ems = True + if hasattr(requester, 'last_ems'): + sec_limit = 3559 + if not (timeutils.is_older_than(requester.last_ems, sec_limit)): + do_ems = False + if do_ems: + na_server = copy.copy(self.connection) + na_server.set_timeout(25) + ems = _create_ems(netapp_backend, app_version, server_type) + try: + if server_type == "cluster": + api_version = na_server.get_api_version() + if api_version: + major, minor = api_version + else: + raise NaApiError(code='Not found', + message='No API version found') + if major == 1 and minor > 15: + node = getattr(requester, 'vserver', None) + else: + node = _get_cluster_node(na_server) + if node is None: + raise NaApiError(code='Not found', + message='No vserver found') + na_server.set_vserver(node) + else: + na_server.set_vfiler(None) + na_server.invoke_successfully(ems, True) + LOG.debug("ems executed successfully.") + except NaApiError as e: + LOG.warning(_LW("Failed to invoke ems. Message : %s") % e) + finally: + requester.last_ems = timeutils.utcnow() diff --git a/cinder/volume/drivers/netapp/client/cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py similarity index 73% rename from cinder/volume/drivers/netapp/client/cmode.py rename to cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index 2a7df882564..2a83e6221fa 100644 --- a/cinder/volume/drivers/netapp/client/cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -1,5 +1,5 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# All Rights Reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -22,19 +22,25 @@ import six from cinder import exception from cinder.i18n import _ from cinder.openstack.common import log as logging -from cinder.volume.drivers.netapp import api as netapp_api -from cinder.volume.drivers.netapp.client import base +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp import utils as na_utils LOG = logging.getLogger(__name__) -class Client(base.Client): +class Client(client_base.Client): - def __init__(self, connection, vserver): - super(Client, self).__init__(connection) - self.vserver = vserver + def __init__(self, **kwargs): + super(Client, self).__init__(**kwargs) + self.vserver = kwargs.get('vserver', None) + self.connection.set_vserver(self.vserver) + + # Default values to run first api + self.connection.set_api_version(1, 15) + (major, minor) = self.get_ontapi_version(cached=False) + self.connection.set_api_version(major, minor) def _invoke_vserver_api(self, na_element, vserver): server = copy.copy(self.connection) @@ -42,6 +48,9 @@ class Client(base.Client): result = server.invoke_successfully(na_element, True) return result + def set_vserver(self, vserver): + self.connection.set_vserver(vserver) + def get_target_details(self): """Gets the target portal details.""" iscsi_if_iter = netapp_api.NaElement('iscsi-interface-get-iter') @@ -74,9 +83,9 @@ class Client(base.Client): return None def get_lun_list(self): - """Gets the list of luns on filer. + """Gets the list of LUNs on filer. - Gets the luns from cluster with vserver. + Gets the LUNs from cluster with vserver. """ luns = [] @@ -102,7 +111,7 @@ class Client(base.Client): return luns def get_lun_map(self, path): - """Gets the lun map by lun path.""" + """Gets the LUN map by LUN path.""" tag = None map_list = [] while True: @@ -188,7 +197,7 @@ class Client(base.Client): zbc = block_count if z_calls == 0: z_calls = 1 - for call in range(0, z_calls): + for _call in range(0, z_calls): if zbc > z_limit: block_count = z_limit zbc -= z_limit @@ -203,7 +212,7 @@ class Client(base.Client): block_ranges = netapp_api.NaElement("block-ranges") segments = int(math.ceil(block_count / float(bc_limit))) bc = block_count - for segment in range(0, segments): + for _segment in range(0, segments): if bc > bc_limit: block_count = bc_limit bc -= bc_limit @@ -225,7 +234,7 @@ class Client(base.Client): self.connection.invoke_successfully(clone_create, True) def get_lun_by_args(self, **args): - """Retrieves lun with specified args.""" + """Retrieves LUN with specified args.""" lun_iter = netapp_api.NaElement('lun-get-iter') lun_iter.add_new_child('max-records', '100') query = netapp_api.NaElement('query') @@ -236,7 +245,7 @@ class Client(base.Client): return attr_list.get_children() def file_assign_qos(self, flex_vol, qos_policy_group, file_path): - """Retrieves lun with specified args.""" + """Retrieves LUN with specified args.""" file_assign_qos = netapp_api.NaElement.create_node_with_children( 'file-assign-qos', **{'volume': flex_vol, @@ -252,7 +261,8 @@ class Client(base.Client): query = netapp_api.NaElement('query') net_if_iter.add_child_elem(query) query.add_node_with_children( - 'net-interface-info', **{'address': na_utils.resolve_hostname(ip)}) + 'net-interface-info', + **{'address': na_utils.resolve_hostname(ip)}) result = self.connection.invoke_successfully(net_if_iter, True) num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: @@ -316,3 +326,79 @@ class Client(base.Client): LOG.debug('file-usage for path %(path)s is %(bytes)s' % {'path': path, 'bytes': unique_bytes}) return unique_bytes + + def get_vserver_ips(self, vserver): + """Get ips for the vserver.""" + result = netapp_api.invoke_api( + self.connection, api_name='net-interface-get-iter', + is_iter=True, tunnel=vserver) + if_list = [] + for res in result: + records = res.get_child_content('num-records') + if records > 0: + attr_list = res['attributes-list'] + ifs = attr_list.get_children() + if_list.extend(ifs) + return if_list + + def check_apis_on_cluster(self, api_list=None): + """Checks API availability and permissions on cluster. + + Checks API availability and permissions for executing user. + Returns a list of failed apis. + """ + api_list = api_list or [] + failed_apis = [] + if api_list: + api_version = self.connection.get_api_version() + if api_version: + major, minor = api_version + if major == 1 and minor < 20: + for api_name in api_list: + na_el = netapp_api.NaElement(api_name) + try: + self.connection.invoke_successfully(na_el) + except Exception as e: + if isinstance(e, netapp_api.NaApiError): + if (e.code == netapp_api.NaErrors + ['API_NOT_FOUND'].code or + e.code == netapp_api.NaErrors + ['INSUFFICIENT_PRIVS'].code): + failed_apis.append(api_name) + elif major == 1 and minor >= 20: + failed_apis = copy.copy(api_list) + result = netapp_api.invoke_api( + self.connection, + api_name='system-user-capability-get-iter', + api_family='cm', + additional_elems=None, + is_iter=True) + for res in result: + attr_list = res.get_child_by_name('attributes-list') + if attr_list: + capabilities = attr_list.get_children() + for capability in capabilities: + op_list = capability.get_child_by_name( + 'operation-list') + if op_list: + ops = op_list.get_children() + for op in ops: + apis = op.get_child_content( + 'api-name') + if apis: + api_list = apis.split(',') + for api_name in api_list: + if (api_name and + api_name.strip() + in failed_apis): + failed_apis.remove( + api_name) + else: + continue + else: + msg = _("Unsupported Clustered Data ONTAP version.") + raise exception.VolumeBackendAPIException(data=msg) + else: + msg = _("Data ONTAP API version could not be determined.") + raise exception.VolumeBackendAPIException(data=msg) + return failed_apis diff --git a/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py b/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py new file mode 100644 index 00000000000..db35c163d11 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py @@ -0,0 +1,83 @@ +# Copyright (c) 2014 Clinton Knight. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp Data ONTAP (7-mode) iSCSI storage systems. +""" + +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.netapp.dataontap.block_7mode import \ + NetAppBlockStorage7modeLibrary as lib_7mode + + +LOG = logging.getLogger(__name__) + + +class NetApp7modeISCSIDriver(driver.ISCSIDriver): + """NetApp 7-mode iSCSI volume driver.""" + + DRIVER_NAME = 'NetApp_iSCSI_7mode_direct' + + def __init__(self, *args, **kwargs): + super(NetApp7modeISCSIDriver, self).__init__(*args, **kwargs) + self.library = lib_7mode(self.DRIVER_NAME, 'iSCSI', **kwargs) + + def do_setup(self, context): + self.library.do_setup(context) + + def check_for_setup_error(self): + self.library.check_for_setup_error() + + def create_volume(self, volume): + self.library.create_volume(volume) + + def create_volume_from_snapshot(self, volume, snapshot): + self.library.create_volume_from_snapshot(volume, snapshot) + + def create_cloned_volume(self, volume, src_vref): + self.library.create_cloned_volume(volume, src_vref) + + def delete_volume(self, volume): + self.library.delete_volume(volume) + + def create_snapshot(self, snapshot): + self.library.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + self.library.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + return self.library.get_volume_stats(refresh) + + def extend_volume(self, volume, new_size): + self.library.extend_volume(volume, new_size) + + def ensure_export(self, context, volume): + return self.library.ensure_export(context, volume) + + def create_export(self, context, volume): + return self.library.create_export(context, volume) + + def remove_export(self, context, volume): + self.library.remove_export(context, volume) + + def initialize_connection(self, volume, connector): + return self.library.initialize_connection_iscsi(volume, connector) + + def terminate_connection(self, volume, connector, **kwargs): + return self.library.terminate_connection_iscsi(volume, connector, + **kwargs) + + def get_pool(self, volume): + return self.library.get_pool(volume) diff --git a/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py new file mode 100644 index 00000000000..ac3396d8694 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py @@ -0,0 +1,83 @@ +# Copyright (c) 2014 Clinton Knight. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp Data ONTAP (C-mode) iSCSI storage systems. +""" + +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.netapp.dataontap.block_cmode import \ + NetAppBlockStorageCmodeLibrary as lib_cmode + + +LOG = logging.getLogger(__name__) + + +class NetAppCmodeISCSIDriver(driver.ISCSIDriver): + """NetApp C-mode iSCSI volume driver.""" + + DRIVER_NAME = 'NetApp_iSCSI_Cluster_direct' + + def __init__(self, *args, **kwargs): + super(NetAppCmodeISCSIDriver, self).__init__(*args, **kwargs) + self.library = lib_cmode(self.DRIVER_NAME, 'iSCSI', **kwargs) + + def do_setup(self, context): + self.library.do_setup(context) + + def check_for_setup_error(self): + self.library.check_for_setup_error() + + def create_volume(self, volume): + self.library.create_volume(volume) + + def create_volume_from_snapshot(self, volume, snapshot): + self.library.create_volume_from_snapshot(volume, snapshot) + + def create_cloned_volume(self, volume, src_vref): + self.library.create_cloned_volume(volume, src_vref) + + def delete_volume(self, volume): + self.library.delete_volume(volume) + + def create_snapshot(self, snapshot): + self.library.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + self.library.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + return self.library.get_volume_stats(refresh) + + def extend_volume(self, volume, new_size): + self.library.extend_volume(volume, new_size) + + def ensure_export(self, context, volume): + return self.library.ensure_export(context, volume) + + def create_export(self, context, volume): + return self.library.create_export(context, volume) + + def remove_export(self, context, volume): + self.library.remove_export(context, volume) + + def initialize_connection(self, volume, connector): + return self.library.initialize_connection_iscsi(volume, connector) + + def terminate_connection(self, volume, connector, **kwargs): + return self.library.terminate_connection_iscsi(volume, connector, + **kwargs) + + def get_pool(self, volume): + return self.library.get_pool(volume) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py new file mode 100644 index 00000000000..0f4740d5607 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py @@ -0,0 +1,215 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Bob Callaway. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp NFS storage. +""" + +from oslo.utils import units +import six + +from cinder import exception +from cinder.i18n import _, _LE, _LI +from cinder.openstack.common import log as logging +from cinder.volume.drivers.netapp.dataontap.client import client_7mode +from cinder.volume.drivers.netapp.dataontap import nfs_base +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume import utils as volume_utils + + +LOG = logging.getLogger(__name__) + + +class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): + """NetApp NFS driver for Data ONTAP (7-mode).""" + + def __init__(self, *args, **kwargs): + super(NetApp7modeNfsDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Do the customized set up on client if any for 7 mode.""" + super(NetApp7modeNfsDriver, self).do_setup(context) + + self.zapi_client = client_7mode.Client( + transport_type=self.configuration.netapp_transport_type, + username=self.configuration.netapp_login, + password=self.configuration.netapp_password, + hostname=self.configuration.netapp_server_hostname, + port=self.configuration.netapp_server_port) + + def check_for_setup_error(self): + """Checks if setup occurred properly.""" + api_version = self.zapi_client.get_ontapi_version() + if api_version: + major, minor = api_version + if major == 1 and minor < 9: + msg = _("Unsupported Data ONTAP version." + " Data ONTAP version 7.3.1 and above is supported.") + raise exception.VolumeBackendAPIException(data=msg) + else: + msg = _("Data ONTAP API version could not be determined.") + raise exception.VolumeBackendAPIException(data=msg) + super(NetApp7modeNfsDriver, self).check_for_setup_error() + + def create_volume(self, volume): + """Creates a volume. + + :param volume: volume reference + """ + LOG.debug('create_volume on %s' % volume['host']) + self._ensure_shares_mounted() + + # get share as pool name + share = volume_utils.extract_host(volume['host'], level='pool') + + if share is None: + msg = _("Pool is not available in the volume host field.") + raise exception.InvalidHost(reason=msg) + + volume['provider_location'] = share + LOG.info(_LI('Creating volume at location %s') + % volume['provider_location']) + + try: + self._do_create_volume(volume) + except Exception as ex: + LOG.error(_LE("Exception creating vol %(name)s on " + "share %(share)s. Details: %(ex)s") + % {'name': volume['name'], + 'share': volume['provider_location'], + 'ex': six.text_type(ex)}) + msg = _("Volume %s could not be created on shares.") + raise exception.VolumeBackendAPIException( + data=msg % (volume['name'])) + + return {'provider_location': volume['provider_location']} + + def _clone_volume(self, volume_name, clone_name, + volume_id, share=None): + """Clones mounted volume with NetApp filer.""" + (_host_ip, export_path) = self._get_export_ip_path(volume_id, share) + storage_path = self.zapi_client.get_actual_path_for_export(export_path) + target_path = '%s/%s' % (storage_path, clone_name) + self.zapi_client.clone_file('%s/%s' % (storage_path, volume_name), + target_path) + + def _update_volume_stats(self): + """Retrieve stats info from vserver.""" + + self._ensure_shares_mounted() + + LOG.debug('Updating volume stats') + data = {} + netapp_backend = 'NetApp_NFS_7mode_direct' + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or netapp_backend + data['vendor_name'] = 'NetApp' + data['driver_version'] = self.VERSION + data['storage_protocol'] = 'nfs' + data['pools'] = self._get_pool_stats() + + self._spawn_clean_cache_job() + self.zapi_client.provide_ems(self, netapp_backend, self._app_version, + server_type="7mode") + self._stats = data + + def _get_pool_stats(self): + """Retrieve pool (i.e. NFS share) stats info from SSC volumes.""" + + pools = [] + + for nfs_share in self._mounted_shares: + + capacity = self._get_extended_capacity_info(nfs_share) + + pool = dict() + pool['pool_name'] = nfs_share + pool['QoS_support'] = False + pool['reserved_percentage'] = 0 + + # Report pool as reserved when over the configured used_ratio + if capacity['used_ratio'] > self.configuration.nfs_used_ratio: + pool['reserved_percentage'] = 100 + + # Report pool as reserved when over the subscribed ratio + if capacity['subscribed_ratio'] >=\ + self.configuration.nfs_oversub_ratio: + pool['reserved_percentage'] = 100 + + # convert sizes to GB + total = float(capacity['apparent_size']) / units.Gi + pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') + + free = float(capacity['apparent_available']) / units.Gi + pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') + + pools.append(pool) + + return pools + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + file_list = [] + exp_volume = self.zapi_client.get_actual_path_for_export(share) + for file in old_files: + path = '/vol/%s/%s' % (exp_volume, file) + u_bytes = self.zapi_client.get_file_usage(path) + file_list.append((file, u_bytes)) + LOG.debug('Shortlisted files eligible for deletion: %s', file_list) + return file_list + + def _is_filer_ip(self, ip): + """Checks whether ip is on the same filer.""" + try: + ifconfig = self.zapi_client.get_ifconfig() + if_info = ifconfig.get_child_by_name('interface-config-info') + if if_info: + ifs = if_info.get_children() + for intf in ifs: + v4_addr = intf.get_child_by_name('v4-primary-address') + if v4_addr: + ip_info = v4_addr.get_child_by_name('ip-address-info') + if ip_info: + address = ip_info.get_child_content('address') + if ip == address: + return True + else: + continue + except Exception: + return False + return False + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + if self._is_filer_ip(ip) and shares: + for share in shares: + ip_sh = share.split(':')[0] + if self._is_filer_ip(ip_sh): + LOG.debug('Share match found for ip %s', ip) + return share + LOG.debug('No share match found for ip %s', ip) + return None + + def _is_share_vol_compatible(self, volume, share): + """Checks if share is compatible with volume to host it.""" + return self._is_share_eligible(share, volume['size']) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py new file mode 100644 index 00000000000..6a334f27041 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -0,0 +1,682 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Bob Callaway. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp NFS storage. +""" + +import os +import re +from threading import Timer +import time + +from oslo.concurrency import processutils +from oslo.utils import excutils +from oslo.utils import units +import six.moves.urllib.parse as urlparse + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.image import image_utils +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume.drivers import nfs + + +LOG = logging.getLogger(__name__) + + +class NetAppNfsDriver(nfs.NfsDriver): + """Base class for NetApp NFS driver for Data ONTAP.""" + + # do not increment this as it may be used in volume type definitions + VERSION = "1.0.0" + REQUIRED_FLAGS = ['netapp_login', 'netapp_password', + 'netapp_server_hostname'] + + def __init__(self, *args, **kwargs): + na_utils.validate_instantiation(**kwargs) + self._execute = None + self._context = None + self._app_version = kwargs.pop("app_version", "unknown") + super(NetAppNfsDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(na_opts.netapp_connection_opts) + self.configuration.append_config_values(na_opts.netapp_basicauth_opts) + self.configuration.append_config_values(na_opts.netapp_transport_opts) + self.configuration.append_config_values(na_opts.netapp_img_cache_opts) + + def set_execute(self, execute): + self._execute = execute + + def do_setup(self, context): + super(NetAppNfsDriver, self).do_setup(context) + self._context = context + na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) + + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met.""" + super(NetAppNfsDriver, self).check_for_setup_error() + + def get_pool(self, volume): + """Return pool name where volume resides. + + :param volume: The volume hosted by the driver. + :return: Name of the pool where given volume is hosted. + """ + return volume['provider_location'] + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + vol_size = volume.size + snap_size = snapshot.volume_size + + self._clone_volume(snapshot.name, volume.name, snapshot.volume_id) + share = self._get_volume_location(snapshot.volume_id) + volume['provider_location'] = share + path = self.local_path(volume) + run_as_root = self._execute_as_root + + if self._discover_file_till_timeout(path): + self._set_rw_permissions(path) + if vol_size != snap_size: + try: + self.extend_volume(volume, vol_size) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error( + _LE("Resizing %s failed. Cleaning volume."), + volume.name) + self._execute('rm', path, run_as_root=run_as_root) + else: + raise exception.CinderException( + _("NFS file %s not discovered.") % volume['name']) + + return {'provider_location': volume['provider_location']} + + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + self._clone_volume(snapshot['volume_name'], + snapshot['name'], + snapshot['volume_id']) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + nfs_mount = self._get_provider_location(snapshot.volume_id) + + if self._volume_not_present(nfs_mount, snapshot.name): + return True + + self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name), + run_as_root=self._execute_as_root) + + def _get_volume_location(self, volume_id): + """Returns NFS mount address as :.""" + nfs_server_ip = self._get_host_ip(volume_id) + export_path = self._get_export_path(volume_id) + return nfs_server_ip + ':' + export_path + + def _clone_volume(self, volume_name, clone_name, volume_id, share=None): + """Clones mounted volume using NetApp API.""" + raise NotImplementedError() + + def _get_provider_location(self, volume_id): + """Returns provider location for given volume.""" + volume = self.db.volume_get(self._context, volume_id) + return volume.provider_location + + def _get_host_ip(self, volume_id): + """Returns IP address for the given volume.""" + return self._get_provider_location(volume_id).split(':')[0] + + def _get_export_path(self, volume_id): + """Returns NFS export path for the given volume.""" + return self._get_provider_location(volume_id).split(':')[1] + + def _volume_not_present(self, nfs_mount, volume_name): + """Check if volume exists.""" + try: + self._try_execute('ls', self._get_volume_path(nfs_mount, + volume_name)) + except processutils.ProcessExecutionError: + # If the volume isn't present + return True + return False + + def _try_execute(self, *command, **kwargs): + # NOTE(vish): Volume commands can partially fail due to timing, but + # running them a second time on failure will usually + # recover nicely. + tries = 0 + while True: + try: + self._execute(*command, **kwargs) + return True + except processutils.ProcessExecutionError: + tries += 1 + if tries >= self.configuration.num_shell_tries: + raise + LOG.exception(_LE("Recovering from a failed execute. " + "Try number %s"), tries) + time.sleep(tries ** 2) + + def _get_volume_path(self, nfs_share, volume_name): + """Get volume path (local fs path) for given volume name on given nfs + share. + + @param nfs_share string, example 172.18.194.100:/var/nfs + @param volume_name string, + example volume-91ee65ec-c473-4391-8c09-162b00c68a8c + """ + + return os.path.join(self._get_mount_point_for_share(nfs_share), + volume_name) + + def create_cloned_volume(self, volume, src_vref): + """Creates a clone of the specified volume.""" + vol_size = volume.size + src_vol_size = src_vref.size + self._clone_volume(src_vref.name, volume.name, src_vref.id) + share = self._get_volume_location(src_vref.id) + volume['provider_location'] = share + path = self.local_path(volume) + + if self._discover_file_till_timeout(path): + self._set_rw_permissions(path) + if vol_size != src_vol_size: + try: + self.extend_volume(volume, vol_size) + except Exception as e: + LOG.error( + _LE("Resizing %s failed. Cleaning volume."), + volume.name) + self._execute('rm', path, + run_as_root=self._execute_as_root) + raise e + else: + raise exception.CinderException( + _("NFS file %s not discovered.") % volume['name']) + + return {'provider_location': volume['provider_location']} + + def _update_volume_stats(self): + """Retrieve stats info from volume group.""" + raise NotImplementedError() + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + super(NetAppNfsDriver, self).copy_image_to_volume( + context, volume, image_service, image_id) + LOG.info(_LI('Copied image to volume %s using regular download.'), + volume['name']) + self._register_image_in_cache(volume, image_id) + + def _register_image_in_cache(self, volume, image_id): + """Stores image in the cache.""" + file_name = 'img-cache-%s' % image_id + LOG.info(_LI("Registering image in cache %s"), file_name) + try: + self._do_clone_rel_img_cache( + volume['name'], file_name, + volume['provider_location'], file_name) + except Exception as e: + LOG.warning(_LW('Exception while registering image %(image_id)s' + ' in cache. Exception: %(exc)s') + % {'image_id': image_id, 'exc': e.__str__()}) + + def _find_image_in_cache(self, image_id): + """Finds image in cache and returns list of shares with file name.""" + result = [] + if getattr(self, '_mounted_shares', None): + for share in self._mounted_shares: + dir = self._get_mount_point_for_share(share) + file_name = 'img-cache-%s' % image_id + file_path = '%s/%s' % (dir, file_name) + if os.path.exists(file_path): + LOG.debug('Found cache file for image %(image_id)s' + ' on share %(share)s' + % {'image_id': image_id, 'share': share}) + result.append((share, file_name)) + return result + + def _do_clone_rel_img_cache(self, src, dst, share, cache_file): + """Do clone operation w.r.t image cache file.""" + @utils.synchronized(cache_file, external=True) + def _do_clone(): + dir = self._get_mount_point_for_share(share) + file_path = '%s/%s' % (dir, dst) + if not os.path.exists(file_path): + LOG.info(_LI('Cloning from cache to destination %s'), dst) + self._clone_volume(src, dst, volume_id=None, share=share) + _do_clone() + + @utils.synchronized('clean_cache') + def _spawn_clean_cache_job(self): + """Spawns a clean task if not running.""" + if getattr(self, 'cleaning', None): + LOG.debug('Image cache cleaning in progress. Returning... ') + return + else: + # Set cleaning to True + self.cleaning = True + t = Timer(0, self._clean_image_cache) + t.start() + + def _clean_image_cache(self): + """Clean the image cache files in cache of space crunch.""" + try: + LOG.debug('Image cache cleaning in progress.') + thres_size_perc_start =\ + self.configuration.thres_avl_size_perc_start + thres_size_perc_stop = \ + self.configuration.thres_avl_size_perc_stop + for share in getattr(self, '_mounted_shares', []): + try: + total_size, total_avl, _total_alc = \ + self._get_capacity_info(share) + avl_percent = int((total_avl / total_size) * 100) + if avl_percent <= thres_size_perc_start: + LOG.info(_LI('Cleaning cache for share %s.'), share) + eligible_files = self._find_old_cache_files(share) + threshold_size = int( + (thres_size_perc_stop * total_size) / 100) + bytes_to_free = int(threshold_size - total_avl) + LOG.debug('Files to be queued for deletion %s', + eligible_files) + self._delete_files_till_bytes_free( + eligible_files, share, bytes_to_free) + else: + continue + except Exception as e: + LOG.warning(_LW('Exception during cache cleaning' + ' %(share)s. Message - %(ex)s') + % {'share': share, 'ex': e.__str__()}) + continue + finally: + LOG.debug('Image cache cleaning done.') + self.cleaning = False + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + raise NotImplementedError() + + def _find_old_cache_files(self, share): + """Finds the old files in cache.""" + mount_fs = self._get_mount_point_for_share(share) + threshold_minutes = self.configuration.expiry_thres_minutes + cmd = ['find', mount_fs, '-maxdepth', '1', '-name', + 'img-cache*', '-amin', '+%s' % threshold_minutes] + res, _err = self._execute(*cmd, run_as_root=self._execute_as_root) + if res: + old_file_paths = res.strip('\n').split('\n') + mount_fs_len = len(mount_fs) + old_files = [x[mount_fs_len + 1:] for x in old_file_paths] + eligible_files = self._shortlist_del_eligible_files( + share, old_files) + return eligible_files + return [] + + def _delete_files_till_bytes_free(self, file_list, share, bytes_to_free=0): + """Delete files from disk till bytes are freed or list exhausted.""" + LOG.debug('Bytes to free %s', bytes_to_free) + if file_list and bytes_to_free > 0: + sorted_files = sorted(file_list, key=lambda x: x[1], reverse=True) + mount_fs = self._get_mount_point_for_share(share) + for f in sorted_files: + if f: + file_path = '%s/%s' % (mount_fs, f[0]) + LOG.debug('Delete file path %s', file_path) + + @utils.synchronized(f[0], external=True) + def _do_delete(): + if self._delete_file(file_path): + return True + return False + + if _do_delete(): + bytes_to_free -= int(f[1]) + if bytes_to_free <= 0: + return + + def _delete_file(self, path): + """Delete file from disk and return result as boolean.""" + try: + LOG.debug('Deleting file at path %s', path) + cmd = ['rm', '-f', path] + self._execute(*cmd, run_as_root=self._execute_as_root) + return True + except Exception as ex: + LOG.warning(_LW('Exception during deleting %s'), ex.__str__()) + return False + + def clone_image(self, volume, image_location, image_id, image_meta): + """Create a volume efficiently from an existing image. + + image_location is a string whose format depends on the + image service backend in use. The driver should use it + to determine whether cloning is possible. + + image_id is a string which represents id of the image. + It can be used by the driver to introspect internal + stores or registry to do an efficient image clone. + + Returns a dict of volume properties eg. provider_location, + boolean indicating whether cloning occurred. + """ + + cloned = False + post_clone = False + try: + cache_result = self._find_image_in_cache(image_id) + if cache_result: + cloned = self._clone_from_cache(volume, image_id, cache_result) + else: + cloned = self._direct_nfs_clone(volume, image_location, + image_id) + if cloned: + post_clone = self._post_clone_image(volume) + except Exception as e: + msg = e.msg if getattr(e, 'msg', None) else e.__str__() + LOG.info(_LI('Image cloning unsuccessful for image' + ' %(image_id)s. Message: %(msg)s') + % {'image_id': image_id, 'msg': msg}) + vol_path = self.local_path(volume) + volume['provider_location'] = None + if os.path.exists(vol_path): + self._delete_file(vol_path) + finally: + cloned = cloned and post_clone + share = volume['provider_location'] if cloned else None + bootable = True if cloned else False + return {'provider_location': share, 'bootable': bootable}, cloned + + def _clone_from_cache(self, volume, image_id, cache_result): + """Clones a copy from image cache.""" + cloned = False + LOG.info(_LI('Cloning image %s from cache'), image_id) + for res in cache_result: + # Repeat tries in other shares if failed in some + (share, file_name) = res + LOG.debug('Cache share: %s', share) + if (share and + self._is_share_vol_compatible(volume, share)): + try: + self._do_clone_rel_img_cache( + file_name, volume['name'], share, file_name) + cloned = True + volume['provider_location'] = share + break + except Exception: + LOG.warning(_LW('Unexpected exception during' + ' image cloning in share %s'), share) + return cloned + + def _direct_nfs_clone(self, volume, image_location, image_id): + """Clone directly in nfs share.""" + LOG.info(_LI('Checking image clone %s from glance share.'), image_id) + cloned = False + image_location = self._construct_image_nfs_url(image_location) + share = self._is_cloneable_share(image_location) + run_as_root = self._execute_as_root + + if share and self._is_share_vol_compatible(volume, share): + LOG.debug('Share is cloneable %s', share) + volume['provider_location'] = share + (__, ___, img_file) = image_location.rpartition('/') + dir_path = self._get_mount_point_for_share(share) + img_path = '%s/%s' % (dir_path, img_file) + img_info = image_utils.qemu_img_info(img_path, + run_as_root=run_as_root) + if img_info.file_format == 'raw': + LOG.debug('Image is raw %s', image_id) + self._clone_volume( + img_file, volume['name'], + volume_id=None, share=share) + cloned = True + else: + LOG.info( + _LI('Image will locally be converted to raw %s'), + image_id) + dst = '%s/%s' % (dir_path, volume['name']) + image_utils.convert_image(img_path, dst, 'raw', + run_as_root=run_as_root) + data = image_utils.qemu_img_info(dst, run_as_root=run_as_root) + if data.file_format != "raw": + raise exception.InvalidResults( + _("Converted to raw, but" + " format is now %s") % data.file_format) + else: + cloned = True + self._register_image_in_cache( + volume, image_id) + return cloned + + def _post_clone_image(self, volume): + """Do operations post image cloning.""" + LOG.info(_LI('Performing post clone for %s'), volume['name']) + vol_path = self.local_path(volume) + if self._discover_file_till_timeout(vol_path): + self._set_rw_permissions(vol_path) + self._resize_image_file(vol_path, volume['size']) + return True + raise exception.InvalidResults( + _("NFS file could not be discovered.")) + + def _resize_image_file(self, path, new_size): + """Resize the image file on share to new size.""" + LOG.debug('Checking file for resize') + if self._is_file_size_equal(path, new_size): + return + else: + LOG.info(_LI('Resizing file to %sG'), new_size) + image_utils.resize_image(path, new_size, + run_as_root=self._execute_as_root) + if self._is_file_size_equal(path, new_size): + return + else: + raise exception.InvalidResults( + _('Resizing image file failed.')) + + def _is_file_size_equal(self, path, size): + """Checks if file size at path is equal to size.""" + data = image_utils.qemu_img_info(path, + run_as_root=self._execute_as_root) + virt_size = data.virtual_size / units.Gi + if virt_size == size: + return True + else: + return False + + def _discover_file_till_timeout(self, path, timeout=45): + """Checks if file size at path is equal to size.""" + # Sometimes nfs takes time to discover file + # Retrying in case any unexpected situation occurs + retry_seconds = timeout + sleep_interval = 2 + while True: + if os.path.exists(path): + return True + else: + if retry_seconds <= 0: + LOG.warning(_LW('Discover file retries exhausted.')) + return False + else: + time.sleep(sleep_interval) + retry_seconds -= sleep_interval + + def _is_cloneable_share(self, image_location): + """Finds if the image at location is cloneable.""" + conn, dr = self._check_get_nfs_path_segs(image_location) + return self._check_share_in_use(conn, dr) + + def _check_get_nfs_path_segs(self, image_location): + """Checks if the nfs path format is matched. + + WebNFS url format with relative-path is supported. + Accepting all characters in path-names and checking + against the mounted shares which will contain only + allowed path segments. Returns connection and dir details. + """ + conn, dr = None, None + if image_location: + nfs_loc_pattern = \ + ('^nfs://(([\w\-\.]+:{1}[\d]+|[\w\-\.]+)(/[^\/].*)' + '*(/[^\/\\\\]+)$)') + matched = re.match(nfs_loc_pattern, image_location, flags=0) + if not matched: + LOG.debug('Image location not in the' + ' expected format %s', image_location) + else: + conn = matched.group(2) + dr = matched.group(3) or '/' + return conn, dr + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + raise NotImplementedError() + + def _check_share_in_use(self, conn, dir): + """Checks if share is cinder mounted and returns it.""" + try: + if conn: + host = conn.split(':')[0] + ip = na_utils.resolve_hostname(host) + share_candidates = [] + for sh in self._mounted_shares: + sh_exp = sh.split(':')[1] + if sh_exp == dir: + share_candidates.append(sh) + if share_candidates: + LOG.debug('Found possible share matches %s', + share_candidates) + return self._share_match_for_ip(ip, share_candidates) + except Exception: + LOG.warning(_LW("Unexpected exception while " + "short listing used share.")) + return None + + def _construct_image_nfs_url(self, image_location): + """Construct direct url for nfs backend. + + It creates direct url from image_location + which is a tuple with direct_url and locations. + Returns url with nfs scheme if nfs store + else returns url. It needs to be verified + by backend before use. + """ + + direct_url, locations = image_location + if not direct_url and not locations: + raise exception.NotFound(_('Image location not present.')) + + # Locations will be always a list of one until + # bp multiple-image-locations is introduced + if not locations: + return direct_url + location = locations[0] + url = location['url'] + if not location['metadata']: + return url + location_type = location['metadata'].get('type') + if not location_type or location_type.lower() != "nfs": + return url + share_location = location['metadata'].get('share_location') + mount_point = location['metadata'].get('mount_point') + if not share_location or not mount_point: + return url + url_parse = urlparse.urlparse(url) + abs_path = os.path.join(url_parse.netloc, url_parse.path) + rel_path = os.path.relpath(abs_path, mount_point) + direct_url = "%s/%s" % (share_location, rel_path) + return direct_url + + def extend_volume(self, volume, new_size): + """Extend an existing volume to the new size.""" + LOG.info(_LI('Extending volume %s.'), volume['name']) + path = self.local_path(volume) + self._resize_image_file(path, new_size) + + def _is_share_vol_compatible(self, volume, share): + """Checks if share is compatible with volume to host it.""" + raise NotImplementedError() + + def _check_share_can_hold_size(self, share, size): + """Checks if volume can hold image with size.""" + _tot_size, tot_available, _tot_allocated = self._get_capacity_info( + share) + if tot_available < size: + msg = _("Container size smaller than required file size.") + raise exception.VolumeDriverException(msg) + + def _move_nfs_file(self, source_path, dest_path): + """Moves source to destination.""" + + @utils.synchronized(dest_path, external=True) + def _move_file(src, dst): + if os.path.exists(dst): + LOG.warning(_LW("Destination %s already exists."), dst) + return False + self._execute('mv', src, dst, run_as_root=self._execute_as_root) + return True + + try: + return _move_file(source_path, dest_path) + except Exception as e: + LOG.warning(_LW('Exception moving file %(src)s. Message - %(e)s') + % {'src': source_path, 'e': e}) + return False + + def _get_export_ip_path(self, volume_id=None, share=None): + """Returns export ip and path. + + One of volume id or share is used to return the values. + """ + + if volume_id: + host_ip = self._get_host_ip(volume_id) + export_path = self._get_export_path(volume_id) + elif share: + host_ip = share.split(':')[0] + export_path = share.split(':')[1] + else: + raise exception.InvalidInput( + 'A volume ID or share was not specified.') + return host_ip, export_path + + def _get_extended_capacity_info(self, nfs_share): + """Returns an extended set of share capacity metrics.""" + + total_size, total_available, total_allocated = \ + self._get_capacity_info(nfs_share) + + used_ratio = (total_size - total_available) / total_size + subscribed_ratio = total_allocated / total_size + apparent_size = max(0, total_size * self.configuration.nfs_used_ratio) + apparent_available = max(0, apparent_size - total_allocated) + + return {'total_size': total_size, 'total_available': total_available, + 'total_allocated': total_allocated, 'used_ratio': used_ratio, + 'subscribed_ratio': subscribed_ratio, + 'apparent_size': apparent_size, + 'apparent_available': apparent_available} diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py new file mode 100644 index 00000000000..db267c29fe1 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -0,0 +1,523 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Bob Callaway. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp NFS storage. +""" + +import os +import uuid + +from oslo.utils import units +import six + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.image import image_utils +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import client_cmode +from cinder.volume.drivers.netapp.dataontap import nfs_base +from cinder.volume.drivers.netapp.dataontap import ssc_cmode +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume.drivers.netapp.utils import get_volume_extra_specs +from cinder.volume import utils as volume_utils + + +LOG = logging.getLogger(__name__) + + +class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver): + """NetApp NFS driver for Data ONTAP (Cluster-mode).""" + + REQUIRED_CMODE_FLAGS = ['netapp_vserver'] + + def __init__(self, *args, **kwargs): + super(NetAppCmodeNfsDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(na_opts.netapp_cluster_opts) + self.configuration.append_config_values(na_opts.netapp_nfs_extra_opts) + + def do_setup(self, context): + """Do the customized set up on client for cluster mode.""" + super(NetAppCmodeNfsDriver, self).do_setup(context) + na_utils.check_flags(self.REQUIRED_CMODE_FLAGS, self.configuration) + + self.vserver = self.configuration.netapp_vserver + + self.zapi_client = client_cmode.Client( + transport_type=self.configuration.netapp_transport_type, + username=self.configuration.netapp_login, + password=self.configuration.netapp_password, + hostname=self.configuration.netapp_server_hostname, + port=self.configuration.netapp_server_port, + vserver=self.vserver) + + self.ssc_enabled = True + self.ssc_vols = None + self.stale_vols = set() + + def check_for_setup_error(self): + """Check that the driver is working and can communicate.""" + super(NetAppCmodeNfsDriver, self).check_for_setup_error() + ssc_cmode.check_ssc_api_permissions(self.zapi_client) + + def create_volume(self, volume): + """Creates a volume. + + :param volume: volume reference + """ + LOG.debug('create_volume on %s' % volume['host']) + self._ensure_shares_mounted() + + # get share as pool name + share = volume_utils.extract_host(volume['host'], level='pool') + + if share is None: + msg = _("Pool is not available in the volume host field.") + raise exception.InvalidHost(reason=msg) + + extra_specs = get_volume_extra_specs(volume) + qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ + if extra_specs else None + + # warn on obsolete extra specs + na_utils.log_extra_spec_warnings(extra_specs) + + try: + volume['provider_location'] = share + LOG.info(_LI('casted to %s') % volume['provider_location']) + self._do_create_volume(volume) + if qos_policy_group: + self._set_qos_policy_group_on_volume(volume, share, + qos_policy_group) + return {'provider_location': volume['provider_location']} + except Exception as ex: + LOG.error(_LW("Exception creating vol %(name)s on " + "share %(share)s. Details: %(ex)s") + % {'name': volume['name'], + 'share': volume['provider_location'], + 'ex': ex}) + volume['provider_location'] = None + finally: + if self.ssc_enabled: + self._update_stale_vols(self._get_vol_for_share(share)) + + msg = _("Volume %s could not be created on shares.") + raise exception.VolumeBackendAPIException(data=msg % (volume['name'])) + + def _set_qos_policy_group_on_volume(self, volume, share, qos_policy_group): + target_path = '%s' % (volume['name']) + export_path = share.split(':')[1] + flex_vol_name = self.zapi_client.get_vol_by_junc_vserver(self.vserver, + export_path) + self.zapi_client.file_assign_qos(flex_vol_name, + qos_policy_group, + target_path) + + def _clone_volume(self, volume_name, clone_name, + volume_id, share=None): + """Clones mounted volume on NetApp Cluster.""" + (vserver, exp_volume) = self._get_vserver_and_exp_vol(volume_id, share) + self.zapi_client.clone_file(exp_volume, volume_name, clone_name, + vserver) + share = share if share else self._get_provider_location(volume_id) + self._post_prov_deprov_in_ssc(share) + + def _get_vserver_and_exp_vol(self, volume_id=None, share=None): + """Gets the vserver and export volume for share.""" + (host_ip, export_path) = self._get_export_ip_path(volume_id, share) + ifs = self.zapi_client.get_if_info_by_ip(host_ip) + vserver = ifs[0].get_child_content('vserver') + exp_volume = self.zapi_client.get_vol_by_junc_vserver(vserver, + export_path) + return vserver, exp_volume + + def _update_volume_stats(self): + """Retrieve stats info from vserver.""" + + self._ensure_shares_mounted() + sync = True if self.ssc_vols is None else False + ssc_cmode.refresh_cluster_ssc(self, self.zapi_client.connection, + self.vserver, synchronous=sync) + + LOG.debug('Updating volume stats') + data = {} + netapp_backend = 'NetApp_NFS_Cluster_direct' + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or netapp_backend + data['vendor_name'] = 'NetApp' + data['driver_version'] = self.VERSION + data['storage_protocol'] = 'nfs' + data['pools'] = self._get_pool_stats() + + self._spawn_clean_cache_job() + self.zapi_client.provide_ems(self, netapp_backend, self._app_version) + self._stats = data + + def _get_pool_stats(self): + """Retrieve pool (i.e. NFS share) stats info from SSC volumes.""" + + pools = [] + + for nfs_share in self._mounted_shares: + + capacity = self._get_extended_capacity_info(nfs_share) + + pool = dict() + pool['pool_name'] = nfs_share + pool['QoS_support'] = False + pool['reserved_percentage'] = 0 + + # Report pool as reserved when over the configured used_ratio + if capacity['used_ratio'] > self.configuration.nfs_used_ratio: + pool['reserved_percentage'] = 100 + + # Report pool as reserved when over the subscribed ratio + if capacity['subscribed_ratio'] >=\ + self.configuration.nfs_oversub_ratio: + pool['reserved_percentage'] = 100 + + # convert sizes to GB + total = float(capacity['apparent_size']) / units.Gi + pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') + + free = float(capacity['apparent_available']) / units.Gi + pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') + + # add SSC content if available + vol = self._get_vol_for_share(nfs_share) + if vol and self.ssc_vols: + pool['netapp_raid_type'] = vol.aggr['raid_type'] + pool['netapp_disk_type'] = vol.aggr['disk_type'] + + mirrored = vol in self.ssc_vols['mirrored'] + pool['netapp_mirrored'] = six.text_type(mirrored).lower() + pool['netapp_unmirrored'] = six.text_type(not mirrored).lower() + + dedup = vol in self.ssc_vols['dedup'] + pool['netapp_dedup'] = six.text_type(dedup).lower() + pool['netapp_nodedup'] = six.text_type(not dedup).lower() + + compression = vol in self.ssc_vols['compression'] + pool['netapp_compression'] = six.text_type(compression).lower() + pool['netapp_nocompression'] = six.text_type( + not compression).lower() + + thin = vol in self.ssc_vols['thin'] + pool['netapp_thin_provisioned'] = six.text_type(thin).lower() + pool['netapp_thick_provisioned'] = six.text_type( + not thin).lower() + + pools.append(pool) + + return pools + + @utils.synchronized('update_stale') + def _update_stale_vols(self, volume=None, reset=False): + """Populates stale vols with vol and returns set copy.""" + if volume: + self.stale_vols.add(volume) + set_copy = self.stale_vols.copy() + if reset: + self.stale_vols.clear() + return set_copy + + @utils.synchronized("refresh_ssc_vols") + def refresh_ssc_vols(self, vols): + """Refreshes ssc_vols with latest entries.""" + if not self._mounted_shares: + LOG.warning(_LW("No shares found hence skipping ssc refresh.")) + return + mnt_share_vols = set() + vs_ifs = self.zapi_client.get_vserver_ips(self.vserver) + for vol in vols['all']: + for sh in self._mounted_shares: + host = sh.split(':')[0] + junction = sh.split(':')[1] + ip = na_utils.resolve_hostname(host) + if (self._ip_in_ifs(ip, vs_ifs) and + junction == vol.id['junction_path']): + mnt_share_vols.add(vol) + vol.export['path'] = sh + break + for key in vols.keys(): + vols[key] = vols[key] & mnt_share_vols + self.ssc_vols = vols + + def _ip_in_ifs(self, ip, api_ifs): + """Checks if ip is listed for ifs in API format.""" + if api_ifs is None: + return False + for ifc in api_ifs: + ifc_ip = ifc.get_child_content("address") + if ifc_ip == ip: + return True + return False + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + file_list = [] + (vserver, exp_volume) = self._get_vserver_and_exp_vol( + volume_id=None, share=share) + for file in old_files: + path = '/vol/%s/%s' % (exp_volume, file) + u_bytes = self.zapi_client.get_file_usage(path, vserver) + file_list.append((file, u_bytes)) + LOG.debug('Shortlisted files eligible for deletion: %s', file_list) + return file_list + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + ip_vserver = self._get_vserver_for_ip(ip) + if ip_vserver and shares: + for share in shares: + ip_sh = share.split(':')[0] + sh_vserver = self._get_vserver_for_ip(ip_sh) + if sh_vserver == ip_vserver: + LOG.debug('Share match found for ip %s', ip) + return share + LOG.debug('No share match found for ip %s', ip) + return None + + def _get_vserver_for_ip(self, ip): + """Get vserver for the mentioned ip.""" + try: + ifs = self.zapi_client.get_if_info_by_ip(ip) + vserver = ifs[0].get_child_content('vserver') + return vserver + except Exception: + return None + + def _get_vol_for_share(self, nfs_share): + """Gets the ssc vol with given share.""" + if self.ssc_vols: + for vol in self.ssc_vols['all']: + if vol.export['path'] == nfs_share: + return vol + return None + + def _is_share_vol_compatible(self, volume, share): + """Checks if share is compatible with volume to host it.""" + compatible = self._is_share_eligible(share, volume['size']) + if compatible and self.ssc_enabled: + matched = self._is_share_vol_type_match(volume, share) + compatible = compatible and matched + return compatible + + def _is_share_vol_type_match(self, volume, share): + """Checks if share matches volume type.""" + netapp_vol = self._get_vol_for_share(share) + LOG.debug("Found volume %(vol)s for share %(share)s." + % {'vol': netapp_vol, 'share': share}) + extra_specs = get_volume_extra_specs(volume) + vols = ssc_cmode.get_volumes_for_specs(self.ssc_vols, extra_specs) + return netapp_vol in vols + + def delete_volume(self, volume): + """Deletes a logical volume.""" + share = volume['provider_location'] + super(NetAppCmodeNfsDriver, self).delete_volume(volume) + self._post_prov_deprov_in_ssc(share) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + share = self._get_provider_location(snapshot.volume_id) + super(NetAppCmodeNfsDriver, self).delete_snapshot(snapshot) + self._post_prov_deprov_in_ssc(share) + + def _post_prov_deprov_in_ssc(self, share): + if self.ssc_enabled and share: + netapp_vol = self._get_vol_for_share(share) + if netapp_vol: + self._update_stale_vols(volume=netapp_vol) + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + copy_success = False + try: + major, minor = self.zapi_client.get_ontapi_version() + col_path = self.configuration.netapp_copyoffload_tool_path + if major == 1 and minor >= 20 and col_path: + self._try_copyoffload(context, volume, image_service, image_id) + copy_success = True + LOG.info(_LI('Copied image %(img)s to volume %(vol)s using ' + 'copy offload workflow.') + % {'img': image_id, 'vol': volume['id']}) + else: + LOG.debug("Copy offload either not configured or" + " unsupported.") + except Exception as e: + LOG.exception(_LE('Copy offload workflow unsuccessful. %s'), e) + finally: + if not copy_success: + super(NetAppCmodeNfsDriver, self).copy_image_to_volume( + context, volume, image_service, image_id) + if self.ssc_enabled: + sh = self._get_provider_location(volume['id']) + self._update_stale_vols(self._get_vol_for_share(sh)) + + def _try_copyoffload(self, context, volume, image_service, image_id): + """Tries server side file copy offload.""" + copied = False + cache_result = self._find_image_in_cache(image_id) + if cache_result: + copied = self._copy_from_cache(volume, image_id, cache_result) + if not cache_result or not copied: + self._copy_from_img_service(context, volume, image_service, + image_id) + + def _get_ip_verify_on_cluster(self, host): + """Verifies if host on same cluster and returns ip.""" + ip = na_utils.resolve_hostname(host) + vserver = self._get_vserver_for_ip(ip) + if not vserver: + raise exception.NotFound(_("Unable to locate an SVM that is " + "managing the IP address '%s'") % ip) + return ip + + def _copy_from_cache(self, volume, image_id, cache_result): + """Try copying image file_name from cached file_name.""" + LOG.debug("Trying copy from cache using copy offload.") + copied = False + for res in cache_result: + try: + (share, file_name) = res + LOG.debug("Found cache file_name on share %s.", share) + if share != self._get_provider_location(volume['id']): + col_path = self.configuration.netapp_copyoffload_tool_path + src_ip = self._get_ip_verify_on_cluster( + share.split(':')[0]) + src_path = os.path.join(share.split(':')[1], file_name) + dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip( + volume['id'])) + dst_path = os.path.join( + self._get_export_path(volume['id']), volume['name']) + self._execute(col_path, src_ip, dst_ip, + src_path, dst_path, + run_as_root=self._execute_as_root, + check_exit_code=0) + self._register_image_in_cache(volume, image_id) + LOG.debug("Copied image from cache to volume %s using" + " copy offload.", volume['id']) + else: + self._clone_file_dst_exists(share, file_name, + volume['name'], + dest_exists=True) + LOG.debug("Copied image from cache to volume %s using" + " cloning.", volume['id']) + self._post_clone_image(volume) + copied = True + break + except Exception as e: + LOG.exception(_LE('Error in workflow copy from cache. %s.'), e) + return copied + + def _clone_file_dst_exists(self, share, src_name, dst_name, + dest_exists=False): + """Clone file even if dest exists.""" + (vserver, exp_volume) = self._get_vserver_and_exp_vol(share=share) + self.zapi_client.clone_file(exp_volume, src_name, dst_name, vserver, + dest_exists=dest_exists) + + def _copy_from_img_service(self, context, volume, image_service, + image_id): + """Copies from the image service using copy offload.""" + LOG.debug("Trying copy from image service using copy offload.") + image_loc = image_service.get_location(context, image_id) + image_loc = self._construct_image_nfs_url(image_loc) + conn, dr = self._check_get_nfs_path_segs(image_loc) + if conn: + src_ip = self._get_ip_verify_on_cluster(conn.split(':')[0]) + else: + raise exception.NotFound(_("Source host details not found.")) + (__, ___, img_file) = image_loc.rpartition('/') + src_path = os.path.join(dr, img_file) + dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip( + volume['id'])) + # tmp file is required to deal with img formats + tmp_img_file = six.text_type(uuid.uuid4()) + col_path = self.configuration.netapp_copyoffload_tool_path + img_info = image_service.show(context, image_id) + dst_share = self._get_provider_location(volume['id']) + self._check_share_can_hold_size(dst_share, img_info['size']) + run_as_root = self._execute_as_root + + dst_dir = self._get_mount_point_for_share(dst_share) + dst_img_local = os.path.join(dst_dir, tmp_img_file) + try: + # If src and dst share not equal + if (('%s:%s' % (src_ip, dr)) != + ('%s:%s' % (dst_ip, self._get_export_path(volume['id'])))): + dst_img_serv_path = os.path.join( + self._get_export_path(volume['id']), tmp_img_file) + self._execute(col_path, src_ip, dst_ip, src_path, + dst_img_serv_path, run_as_root=run_as_root, + check_exit_code=0) + else: + self._clone_file_dst_exists(dst_share, img_file, tmp_img_file) + self._discover_file_till_timeout(dst_img_local, timeout=120) + LOG.debug('Copied image %(img)s to tmp file %(tmp)s.' + % {'img': image_id, 'tmp': tmp_img_file}) + dst_img_cache_local = os.path.join(dst_dir, + 'img-cache-%s' % image_id) + if img_info['disk_format'] == 'raw': + LOG.debug('Image is raw %s.', image_id) + self._clone_file_dst_exists(dst_share, tmp_img_file, + volume['name'], dest_exists=True) + self._move_nfs_file(dst_img_local, dst_img_cache_local) + LOG.debug('Copied raw image %(img)s to volume %(vol)s.' + % {'img': image_id, 'vol': volume['id']}) + else: + LOG.debug('Image will be converted to raw %s.', image_id) + img_conv = six.text_type(uuid.uuid4()) + dst_img_conv_local = os.path.join(dst_dir, img_conv) + + # Checking against image size which is approximate check + self._check_share_can_hold_size(dst_share, img_info['size']) + try: + image_utils.convert_image(dst_img_local, + dst_img_conv_local, 'raw', + run_as_root=run_as_root) + data = image_utils.qemu_img_info(dst_img_conv_local, + run_as_root=run_as_root) + if data.file_format != "raw": + raise exception.InvalidResults( + _("Converted to raw, but format is now %s.") + % data.file_format) + else: + self._clone_file_dst_exists(dst_share, img_conv, + volume['name'], + dest_exists=True) + self._move_nfs_file(dst_img_conv_local, + dst_img_cache_local) + LOG.debug('Copied locally converted raw image' + ' %(img)s to volume %(vol)s.' + % {'img': image_id, 'vol': volume['id']}) + finally: + if os.path.exists(dst_img_conv_local): + self._delete_file(dst_img_conv_local) + self._post_clone_image(volume) + finally: + if os.path.exists(dst_img_local): + self._delete_file(dst_img_local) diff --git a/cinder/volume/drivers/netapp/ssc_utils.py b/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py similarity index 89% rename from cinder/volume/drivers/netapp/ssc_utils.py rename to cinder/volume/drivers/netapp/dataontap/ssc_cmode.py index f0798992cb9..197112aee6b 100644 --- a/cinder/volume/drivers/netapp/ssc_utils.py +++ b/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py @@ -1,6 +1,7 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# All Rights Reserved. +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -24,11 +25,10 @@ from oslo.utils import timeutils import six from cinder import exception -from cinder.i18n import _, _LW +from cinder.i18n import _, _LI, _LW from cinder.openstack.common import log as logging from cinder import utils -from cinder.volume import driver -from cinder.volume.drivers.netapp import api +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp import utils as na_utils @@ -155,11 +155,11 @@ def query_cluster_vols_for_ssc(na_server, vserver, volume=None): 'volume-space-attributes', 'volume-state-attributes', 'volume-qos-attributes']} - result = na_utils.invoke_api(na_server, api_name='volume-get-iter', - api_family='cm', query=query, - des_result=des_attr, - additional_elems=None, - is_iter=True) + result = netapp_api.invoke_api(na_server, api_name='volume-get-iter', + api_family='cm', query=query, + des_result=des_attr, + additional_elems=None, + is_iter=True) vols = set() for res in result: records = res.get_child_content('num-records') @@ -256,12 +256,12 @@ def query_aggr_options(na_server, aggr_name): add_elems = {'aggregate': aggr_name} attrs = {} try: - result = na_utils.invoke_api(na_server, - api_name='aggr-options-list-info', - api_family='cm', query=None, - des_result=None, - additional_elems=add_elems, - is_iter=False) + result = netapp_api.invoke_api(na_server, + api_name='aggr-options-list-info', + api_family='cm', query=None, + des_result=None, + additional_elems=add_elems, + is_iter=False) for res in result: options = res.get_child_by_name('options') if options: @@ -290,11 +290,11 @@ def get_sis_vol_dict(na_server, vserver, volume=None): query_attr['path'] = vol_path query = {'sis-status-info': query_attr} try: - result = na_utils.invoke_api(na_server, - api_name='sis-get-iter', - api_family='cm', - query=query, - is_iter=True) + result = netapp_api.invoke_api(na_server, + api_name='sis-get-iter', + api_family='cm', + query=query, + is_iter=True) for res in result: attr_list = res.get_child_by_name('attributes-list') if attr_list: @@ -325,10 +325,10 @@ def get_snapmirror_vol_dict(na_server, vserver, volume=None): query_attr['source-volume'] = volume query = {'snapmirror-info': query_attr} try: - result = na_utils.invoke_api(na_server, - api_name='snapmirror-get-iter', - api_family='cm', query=query, - is_iter=True) + result = netapp_api.invoke_api(na_server, + api_name='snapmirror-get-iter', + api_family='cm', query=query, + is_iter=True) for res in result: attr_list = res.get_child_by_name('attributes-list') if attr_list: @@ -359,12 +359,12 @@ def query_aggr_storage_disk(na_server, aggr): des_attr = {'storage-disk-info': {'disk-raid-info': ['effective-disk-type']}} try: - result = na_utils.invoke_api(na_server, - api_name='storage-disk-get-iter', - api_family='cm', query=query, - des_result=des_attr, - additional_elems=None, - is_iter=True) + result = netapp_api.invoke_api(na_server, + api_name='storage-disk-get-iter', + api_family='cm', query=query, + des_result=des_attr, + additional_elems=None, + is_iter=True) for res in result: attr_list = res.get_child_by_name('attributes-list') if attr_list: @@ -421,8 +421,8 @@ def refresh_cluster_stale_ssc(*args, **kwargs): @utils.synchronized(lock_pr) def refresh_stale_ssc(): stale_vols = backend._update_stale_vols(reset=True) - LOG.info(_('Running stale ssc refresh job for %(server)s' - ' and vserver %(vs)s') + LOG.info(_LI('Running stale ssc refresh job for %(server)s' + ' and vserver %(vs)s') % {'server': na_server, 'vs': vserver}) # refreshing single volumes can create inconsistency # hence doing manipulations on copy @@ -455,8 +455,8 @@ def refresh_cluster_stale_ssc(*args, **kwargs): vol_set = ssc_vols_copy[k] vol_set.discard(vol) backend.refresh_ssc_vols(ssc_vols_copy) - LOG.info(_('Successfully completed stale refresh job for' - ' %(server)s and vserver %(vs)s') + LOG.info(_LI('Successfully completed stale refresh job for' + ' %(server)s and vserver %(vs)s') % {'server': na_server, 'vs': vserver}) refresh_stale_ssc() @@ -482,14 +482,14 @@ def get_cluster_latest_ssc(*args, **kwargs): @utils.synchronized(lock_pr) def get_latest_ssc(): - LOG.info(_('Running cluster latest ssc job for %(server)s' - ' and vserver %(vs)s') + LOG.info(_LI('Running cluster latest ssc job for %(server)s' + ' and vserver %(vs)s') % {'server': na_server, 'vs': vserver}) ssc_vols = get_cluster_ssc(na_server, vserver) backend.refresh_ssc_vols(ssc_vols) backend.ssc_run_time = timeutils.utcnow() - LOG.info(_('Successfully completed ssc job for %(server)s' - ' and vserver %(vs)s') + LOG.info(_LI('Successfully completed ssc job for %(server)s' + ' and vserver %(vs)s') % {'server': na_server, 'vs': vserver}) get_latest_ssc() @@ -499,9 +499,7 @@ def get_cluster_latest_ssc(*args, **kwargs): def refresh_cluster_ssc(backend, na_server, vserver, synchronous=False): """Refresh cluster ssc for backend.""" - if not isinstance(backend, driver.VolumeDriver): - raise exception.InvalidInput(reason=_("Backend not a VolumeDriver.")) - if not isinstance(na_server, api.NaServer): + if not isinstance(na_server, netapp_api.NaServer): raise exception.InvalidInput(reason=_("Backend server not NaServer.")) delta_secs = getattr(backend, 'ssc_run_delta_secs', 1800) if getattr(backend, 'ssc_job_running', None): @@ -600,8 +598,8 @@ def get_volumes_for_specs(ssc_vols, specs): return result -def check_ssc_api_permissions(na_server): - """Checks backend ssc api permissions for the user.""" +def check_ssc_api_permissions(client_cmode): + """Checks backend SSC API permissions for the user.""" api_map = {'storage-disk-get-iter': ['netapp:disk_type'], 'snapmirror-get-iter': ['netapp_mirrored', 'netapp_unmirrored'], @@ -610,7 +608,7 @@ def check_ssc_api_permissions(na_server): 'netapp_nocompression'], 'aggr-options-list-info': ['netapp:raid_type'], 'volume-get-iter': []} - failed_apis = na_utils.check_apis_on_cluster(na_server, api_map.keys()) + failed_apis = client_cmode.check_apis_on_cluster(api_map.keys()) if failed_apis: if 'volume-get-iter' in failed_apis: msg = _("Fatal error: User not permitted" @@ -621,6 +619,6 @@ def check_ssc_api_permissions(na_server): for fail in failed_apis: unsupp_ssc_features.extend(api_map[fail]) LOG.warning(_LW("The user does not have access or sufficient " - "privileges to use all netapp apis. The " + "privileges to use all netapp APIs. The " "following extra_specs will fail or be ignored: " "%s"), unsupp_ssc_features) diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 3b144f91f1c..26bcd812cc6 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -1,5 +1,5 @@ -# Copyright (c) 2014 NetApp, Inc. -# All Rights Reserved. +# Copyright (c) 2014 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 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 @@ -22,7 +22,7 @@ import requests import six.moves.urllib.parse as urlparse from cinder import exception -from cinder.i18n import _ +from cinder.i18n import _, _LE from cinder.openstack.common import log as logging @@ -71,8 +71,8 @@ class WebserviceClient(object): # Catching error conditions other than the perceived ones. # Helps propagating only known exceptions back to the caller. except Exception as e: - LOG.exception(_("Unexpected error while invoking web service." - " Error - %s."), e) + LOG.exception(_LE("Unexpected error while invoking web service." + " Error - %s."), e) raise exception.NetAppDriverException( _("Invoking web service failed.")) self._eval_response(response) diff --git a/cinder/volume/drivers/netapp/eseries/iscsi.py b/cinder/volume/drivers/netapp/eseries/iscsi.py index 3c505795370..be26cbbe881 100644 --- a/cinder/volume/drivers/netapp/eseries/iscsi.py +++ b/cinder/volume/drivers/netapp/eseries/iscsi.py @@ -1,5 +1,4 @@ -# Copyright (c) 2014 NetApp, Inc. -# All Rights Reserved. +# Copyright (c) 2014 NetApp, Inc. 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 @@ -31,11 +30,12 @@ from cinder.openstack.common import log as logging from cinder import utils as cinder_utils from cinder.volume import driver from cinder.volume.drivers.netapp.eseries import client +from cinder.volume.drivers.netapp.eseries import utils from cinder.volume.drivers.netapp.options import netapp_basicauth_opts from cinder.volume.drivers.netapp.options import netapp_connection_opts from cinder.volume.drivers.netapp.options import netapp_eseries_opts from cinder.volume.drivers.netapp.options import netapp_transport_opts -from cinder.volume.drivers.netapp import utils +from cinder.volume.drivers.netapp import utils as na_utils from cinder.volume import utils as volume_utils @@ -49,11 +49,11 @@ CONF.register_opts(netapp_eseries_opts) CONF.register_opts(netapp_transport_opts) -class Driver(driver.ISCSIDriver): +class NetAppEseriesISCSIDriver(driver.ISCSIDriver): """Executes commands relating to Volumes.""" VERSION = "1.0.0" - required_flags = ['netapp_server_hostname', 'netapp_controller_ips', + REQUIRED_FLAGS = ['netapp_server_hostname', 'netapp_controller_ips', 'netapp_login', 'netapp_password', 'netapp_storage_pools'] SLEEP_SECS = 5 @@ -80,8 +80,8 @@ class Driver(driver.ISCSIDriver): } def __init__(self, *args, **kwargs): - super(Driver, self).__init__(*args, **kwargs) - utils.validate_instantiation(**kwargs) + super(NetAppEseriesISCSIDriver, self).__init__(*args, **kwargs) + na_utils.validate_instantiation(**kwargs) self.configuration.append_config_values(netapp_basicauth_opts) self.configuration.append_config_values(netapp_connection_opts) self.configuration.append_config_values(netapp_transport_opts) @@ -94,7 +94,8 @@ class Driver(driver.ISCSIDriver): def do_setup(self, context): """Any initialization the volume driver does while starting.""" - self._check_flags() + na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) + port = self.configuration.netapp_server_port scheme = self.configuration.netapp_transport_type.lower() if port is None: @@ -102,6 +103,7 @@ class Driver(driver.ISCSIDriver): port = 8080 elif scheme == 'https': port = 8443 + self._client = client.RestClient( scheme=scheme, host=self.configuration.netapp_server_hostname, @@ -111,36 +113,34 @@ class Driver(driver.ISCSIDriver): password=self.configuration.netapp_password) self._check_mode_get_or_register_storage_system() - def _check_flags(self): - """Ensure that the flags we care about are set.""" - required_flags = self.required_flags - for flag in required_flags: - if not getattr(self.configuration, flag, None): - msg = _('%s is not set.') % flag - raise exception.InvalidInput(reason=msg) - if not self.configuration.use_multipath_for_image_xfer: - msg = _('Production use of "%(backend)s" backend requires the ' - 'Cinder controller to have multipathing properly set up ' - 'and the configuration option "%(mpflag)s" to be set to ' - '"True".') % {'backend': self._backend_name, - 'mpflag': 'use_multipath_for_image_xfer'} - LOG.warning(msg) - def check_for_setup_error(self): + self._check_host_type() + self._check_multipath() + self._check_storage_system() + self._populate_system_objects() + + def _check_host_type(self): self.host_type =\ self.HOST_TYPES.get(self.configuration.netapp_eseries_host_type, None) if not self.host_type: raise exception.NetAppDriverException( _('Configured host type is not supported.')) - self._check_storage_system() - self._populate_system_objects() + + def _check_multipath(self): + if not self.configuration.use_multipath_for_image_xfer: + msg = _LW('Production use of "%(backend)s" backend requires the ' + 'Cinder controller to have multipathing properly set up ' + 'and the configuration option "%(mpflag)s" to be set to ' + '"True".') % {'backend': self._backend_name, + 'mpflag': 'use_multipath_for_image_xfer'} + LOG.warning(msg) def _check_mode_get_or_register_storage_system(self): """Does validity checks for storage system registry and health.""" def _resolve_host(host): try: - ip = utils.resolve_hostname(host) + ip = na_utils.resolve_hostname(host) return ip except socket.gaierror as e: LOG.error(_LE('Error resolving host %(host)s. Error - %(e)s.') @@ -150,7 +150,7 @@ class Driver(driver.ISCSIDriver): ips = self.configuration.netapp_controller_ips ips = [i.strip() for i in ips.split(",")] ips = [x for x in ips if _resolve_host(x)] - host = utils.resolve_hostname( + host = na_utils.resolve_hostname( self.configuration.netapp_server_hostname) if not ips: msg = _('Controller ips not valid after resolution.') @@ -687,14 +687,14 @@ class Driver(driver.ISCSIDriver): raise exception.NotFound(_("Host type %s not supported.") % host_type) def _get_free_lun(self, host, maps=None): - """Gets free lun for given host.""" + """Gets free LUN for given host.""" ref = host['hostRef'] luns = maps or self._get_vol_mapping_for_host_frm_array(ref) used_luns = set(map(lambda lun: int(lun['lun']), luns)) for lun in xrange(self.MAX_LUNS_PER_HOST): if lun not in used_luns: return lun - msg = _("No free luns. Host might exceeded max luns.") + msg = _("No free LUNs. Host might exceeded max LUNs.") raise exception.NetAppDriverException(msg) def _get_vol_mapping_for_host_frm_array(self, host_ref): @@ -806,7 +806,7 @@ class Driver(driver.ISCSIDriver): def _garbage_collect_tmp_vols(self): """Removes tmp vols with no snapshots.""" try: - if not utils.set_safe_attr(self, 'clean_job_running', True): + if not na_utils.set_safe_attr(self, 'clean_job_running', True): LOG.warning(_LW('Returning as clean tmp ' 'vol job already running.')) return @@ -819,4 +819,4 @@ class Driver(driver.ISCSIDriver): LOG.debug("Error deleting vol with label %s.", label) finally: - utils.set_safe_attr(self, 'clean_job_running', False) + na_utils.set_safe_attr(self, 'clean_job_running', False) diff --git a/cinder/volume/drivers/netapp/eseries/utils.py b/cinder/volume/drivers/netapp/eseries/utils.py new file mode 100644 index 00000000000..8ef3655b557 --- /dev/null +++ b/cinder/volume/drivers/netapp/eseries/utils.py @@ -0,0 +1,52 @@ +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Utilities for NetApp E-series drivers. +""" + +import base64 +import binascii +import uuid + +import six + +from cinder.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +def encode_hex_to_base32(hex_string): + """Encodes hex to base32 bit as per RFC4648.""" + bin_form = binascii.unhexlify(hex_string) + return base64.b32encode(bin_form) + + +def decode_base32_to_hex(base32_string): + """Decodes base32 string to hex string.""" + bin_form = base64.b32decode(base32_string) + return binascii.hexlify(bin_form) + + +def convert_uuid_to_es_fmt(uuid_str): + """Converts uuid to e-series compatible name format.""" + uuid_base32 = encode_hex_to_base32(uuid.UUID(six.text_type(uuid_str)).hex) + return uuid_base32.strip('=') + + +def convert_es_fmt_to_uuid(es_label): + """Converts e-series name format to uuid.""" + es_label_b32 = es_label.ljust(32, '=') + return uuid.UUID(binascii.hexlify(base64.b32decode(es_label_b32))) diff --git a/cinder/volume/drivers/netapp/iscsi.py b/cinder/volume/drivers/netapp/iscsi.py deleted file mode 100644 index aa94c4874a5..00000000000 --- a/cinder/volume/drivers/netapp/iscsi.py +++ /dev/null @@ -1,1072 +0,0 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# 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 -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Volume driver for NetApp iSCSI storage systems. - -This driver requires NetApp Clustered Data ONTAP or 7-mode -storage systems with installed iSCSI licenses. -""" - -import copy -import sys -import uuid - -from oslo.utils import excutils -from oslo.utils import timeutils -from oslo.utils import units -import six - -from cinder import exception -from cinder.i18n import _, _LE, _LI, _LW -from cinder.openstack.common import log as logging -from cinder import utils -from cinder.volume import driver -from cinder.volume.drivers.netapp.api import NaApiError -from cinder.volume.drivers.netapp.api import NaElement -from cinder.volume.drivers.netapp.api import NaServer -from cinder.volume.drivers.netapp.client import cmode -from cinder.volume.drivers.netapp.client import seven_mode -from cinder.volume.drivers.netapp.options import netapp_7mode_opts -from cinder.volume.drivers.netapp.options import netapp_basicauth_opts -from cinder.volume.drivers.netapp.options import netapp_cluster_opts -from cinder.volume.drivers.netapp.options import netapp_connection_opts -from cinder.volume.drivers.netapp.options import netapp_provisioning_opts -from cinder.volume.drivers.netapp.options import netapp_transport_opts -from cinder.volume.drivers.netapp import ssc_utils -from cinder.volume.drivers.netapp import utils as na_utils -from cinder.volume.drivers.netapp.utils import get_volume_extra_specs -from cinder.volume.drivers.netapp.utils import round_down -from cinder.volume.drivers.netapp.utils import set_safe_attr -from cinder.volume.drivers.netapp.utils import validate_instantiation -from cinder.volume import utils as volume_utils - - -LOG = logging.getLogger(__name__) - - -class NetAppLun(object): - """Represents a LUN on NetApp storage.""" - - def __init__(self, handle, name, size, metadata_dict): - self.handle = handle - self.name = name - self.size = size - self.metadata = metadata_dict or {} - - def get_metadata_property(self, prop): - """Get the metadata property of a LUN.""" - if prop in self.metadata: - return self.metadata[prop] - name = self.name - msg = _("No metadata property %(prop)s defined for the" - " LUN %(name)s") - msg_fmt = {'prop': prop, 'name': name} - LOG.debug(msg % msg_fmt) - - def __str__(self, *args, **kwargs): - return 'NetApp Lun[handle:%s, name:%s, size:%s, metadata:%s]'\ - % (self.handle, self.name, self.size, self.metadata) - - -class NetAppDirectISCSIDriver(driver.ISCSIDriver): - """NetApp Direct iSCSI volume driver.""" - - # do not increment this as it may be used in volume type definitions - VERSION = "1.0.0" - - IGROUP_PREFIX = 'openstack-' - required_flags = ['netapp_login', 'netapp_password', - 'netapp_server_hostname'] - - def __init__(self, *args, **kwargs): - self._app_version = kwargs.pop("app_version", "unknown") - super(NetAppDirectISCSIDriver, self).__init__(*args, **kwargs) - validate_instantiation(**kwargs) - self.configuration.append_config_values(netapp_connection_opts) - self.configuration.append_config_values(netapp_basicauth_opts) - self.configuration.append_config_values(netapp_transport_opts) - self.configuration.append_config_values(netapp_provisioning_opts) - self.lun_table = {} - self.zapi_client = None - - def _create_client(self, **kwargs): - """Instantiate a client for NetApp server. - - This method creates NetApp server client for api communication. - """ - - host_filer = kwargs['hostname'] - LOG.debug('Using NetApp filer: %s' % host_filer) - self.client = NaServer(host=host_filer, - server_type=NaServer.SERVER_TYPE_FILER, - transport_type=kwargs['transport_type'], - style=NaServer.STYLE_LOGIN_PASSWORD, - username=kwargs['login'], - password=kwargs['password']) - if kwargs['port'] is not None: - self.client.set_port(kwargs['port']) - - def _do_custom_setup(self): - """Does custom setup depending on the type of filer.""" - raise NotImplementedError() - - def _check_flags(self): - """Ensure that the flags we care about are set.""" - required_flags = self.required_flags - for flag in required_flags: - if not getattr(self.configuration, flag, None): - msg = _('%s is not set') % flag - raise exception.InvalidInput(reason=msg) - - def do_setup(self, context): - """Setup the NetApp Volume driver. - - Called one time by the manager after the driver is loaded. - Validate the flags we care about and setup NetApp - client. - """ - - self._check_flags() - self._create_client( - transport_type=self.configuration.netapp_transport_type, - login=self.configuration.netapp_login, - password=self.configuration.netapp_password, - hostname=self.configuration.netapp_server_hostname, - port=self.configuration.netapp_server_port) - self._do_custom_setup() - - def check_for_setup_error(self): - """Check that the driver is working and can communicate. - - Discovers the LUNs on the NetApp server. - """ - - self.lun_table = {} - lun_list = self.zapi_client.get_lun_list() - self._extract_and_populate_luns(lun_list) - LOG.debug("Success getting LUN list from server") - - def get_pool(self, volume): - """Return pool name where volume resides. - - :param volume: The volume hosted by the driver. - :return: Name of the pool where given volume is hosted. - """ - name = volume['name'] - metadata = self._get_lun_attr(name, 'metadata') or dict() - return metadata.get('Volume', None) - - def create_volume(self, volume): - """Driver entry point for creating a new volume (aka ONTAP LUN).""" - - LOG.debug('create_volume on %s' % volume['host']) - - # get ONTAP volume name as pool name - ontap_volume_name = volume_utils.extract_host(volume['host'], - level='pool') - - if ontap_volume_name is None: - msg = _("Pool is not available in the volume host field.") - raise exception.InvalidHost(reason=msg) - - lun_name = volume['name'] - - # start with default size, get requested size - default_size = units.Mi * 100 # 100 MB - size = default_size if not int(volume['size'])\ - else int(volume['size']) * units.Gi - - metadata = {'OsType': 'linux', 'SpaceReserved': 'true'} - - extra_specs = get_volume_extra_specs(volume) - qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ - if extra_specs else None - - # warn on obsolete extra specs - na_utils.log_extra_spec_warnings(extra_specs) - - self.create_lun(ontap_volume_name, lun_name, size, - metadata, qos_policy_group) - LOG.debug('Created LUN with name %s' % lun_name) - - metadata['Path'] = '/vol/%s/%s' % (ontap_volume_name, lun_name) - metadata['Volume'] = ontap_volume_name - metadata['Qtree'] = None - - handle = self._create_lun_handle(metadata) - self._add_lun_to_table(NetAppLun(handle, lun_name, size, metadata)) - - def delete_volume(self, volume): - """Driver entry point for destroying existing volumes.""" - name = volume['name'] - metadata = self._get_lun_attr(name, 'metadata') - if not metadata: - msg = _LW("No entry in LUN table for volume/snapshot %(name)s.") - msg_fmt = {'name': name} - LOG.warning(msg % msg_fmt) - return - self.zapi_client.destroy_lun(metadata['Path']) - self.lun_table.pop(name) - - def ensure_export(self, context, volume): - """Driver entry point to get the export info for an existing volume.""" - handle = self._get_lun_attr(volume['name'], 'handle') - return {'provider_location': handle} - - def create_export(self, context, volume): - """Driver entry point to get the export info for a new volume.""" - handle = self._get_lun_attr(volume['name'], 'handle') - return {'provider_location': handle} - - def remove_export(self, context, volume): - """Driver entry point to remove an export for a volume. - - Since exporting is idempotent in this driver, we have nothing - to do for unexporting. - """ - - pass - - def initialize_connection(self, volume, connector): - """Driver entry point to attach a volume to an instance. - - Do the LUN masking on the storage system so the initiator can access - the LUN on the target. Also return the iSCSI properties so the - initiator can find the LUN. This implementation does not call - _get_iscsi_properties() to get the properties because cannot store the - LUN number in the database. We only find out what the LUN number will - be during this method call so we construct the properties dictionary - ourselves. - """ - - initiator_name = connector['initiator'] - name = volume['name'] - lun_id = self._map_lun(name, initiator_name, 'iscsi', None) - msg = _("Mapped LUN %(name)s to the initiator %(initiator_name)s") - msg_fmt = {'name': name, 'initiator_name': initiator_name} - LOG.debug(msg % msg_fmt) - iqn = self.zapi_client.get_iscsi_service_details() - target_details_list = self.zapi_client.get_target_details() - msg = _("Successfully fetched target details for LUN %(name)s and " - "initiator %(initiator_name)s") - msg_fmt = {'name': name, 'initiator_name': initiator_name} - LOG.debug(msg % msg_fmt) - - if not target_details_list: - msg = _('No iscsi target details were found for LUN %s') - raise exception.VolumeBackendAPIException(data=msg % name) - target_details = None - for tgt_detail in target_details_list: - if tgt_detail.get('interface-enabled', 'true') == 'true': - target_details = tgt_detail - break - if not target_details: - target_details = target_details_list[0] - - if not target_details['address'] and target_details['port']: - msg = _('Failed to get target portal for the LUN %s') - raise exception.VolumeBackendAPIException(data=msg % name) - if not iqn: - msg = _('Failed to get target IQN for the LUN %s') - raise exception.VolumeBackendAPIException(data=msg % name) - - properties = {} - properties['target_discovered'] = False - (address, port) = (target_details['address'], target_details['port']) - properties['target_portal'] = '%s:%s' % (address, port) - properties['target_iqn'] = iqn - properties['target_lun'] = lun_id - properties['volume_id'] = volume['id'] - - auth = volume['provider_auth'] - if auth: - (auth_method, auth_username, auth_secret) = auth.split() - properties['auth_method'] = auth_method - properties['auth_username'] = auth_username - properties['auth_password'] = auth_secret - - return { - 'driver_volume_type': 'iscsi', - 'data': properties, - } - - def create_snapshot(self, snapshot): - """Driver entry point for creating a snapshot. - - This driver implements snapshots by using efficient single-file - (LUN) cloning. - """ - - vol_name = snapshot['volume_name'] - snapshot_name = snapshot['name'] - lun = self._get_lun_from_table(vol_name) - self._clone_lun(lun.name, snapshot_name, 'false') - - def delete_snapshot(self, snapshot): - """Driver entry point for deleting a snapshot.""" - self.delete_volume(snapshot) - LOG.debug("Snapshot %s deletion successful" % snapshot['name']) - - def create_volume_from_snapshot(self, volume, snapshot): - """Driver entry point for creating a new volume from a snapshot. - - Many would call this "cloning" and in fact we use cloning to implement - this feature. - """ - - vol_size = volume['size'] - snap_size = snapshot['volume_size'] - snapshot_name = snapshot['name'] - new_name = volume['name'] - self._clone_lun(snapshot_name, new_name, 'true') - if vol_size != snap_size: - try: - self.extend_volume(volume, volume['size']) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error(_LE("Resizing %s failed. " - "Cleaning volume."), new_name) - self.delete_volume(volume) - - def terminate_connection(self, volume, connector, **kwargs): - """Driver entry point to unattach a volume from an instance. - - Unmask the LUN on the storage system so the given initiator can no - longer access it. - """ - - initiator_name = connector['initiator'] - name = volume['name'] - metadata = self._get_lun_attr(name, 'metadata') - path = metadata['Path'] - self._unmap_lun(path, initiator_name) - msg = _("Unmapped LUN %(name)s from the initiator " - "%(initiator_name)s") - msg_fmt = {'name': name, 'initiator_name': initiator_name} - LOG.debug(msg % msg_fmt) - - def create_lun(self, volume_name, lun_name, size, - metadata, qos_policy_group=None): - """Creates a LUN, handling ONTAP differences as needed.""" - raise NotImplementedError() - - def _create_lun_handle(self, metadata): - """Returns lun handle based on filer type.""" - raise NotImplementedError() - - def _extract_and_populate_luns(self, api_luns): - """Extracts the luns from api. - - Populates in the lun table. - """ - - for lun in api_luns: - meta_dict = self._create_lun_meta(lun) - path = lun.get_child_content('path') - (_rest, _splitter, name) = path.rpartition('/') - handle = self._create_lun_handle(meta_dict) - size = lun.get_child_content('size') - discovered_lun = NetAppLun(handle, name, - size, meta_dict) - self._add_lun_to_table(discovered_lun) - - def _is_naelement(self, elem): - """Checks if element is NetApp element.""" - if not isinstance(elem, NaElement): - raise ValueError('Expects NaElement') - - def _map_lun(self, name, initiator, initiator_type='iscsi', lun_id=None): - """Maps lun to the initiator and returns lun id assigned.""" - metadata = self._get_lun_attr(name, 'metadata') - os = metadata['OsType'] - path = metadata['Path'] - if self._check_allowed_os(os): - os = os - else: - os = 'default' - igroup_name = self._get_or_create_igroup(initiator, - initiator_type, os) - try: - return self.zapi_client.map_lun(path, igroup_name, lun_id=lun_id) - except NaApiError: - exc_info = sys.exc_info() - (_igroup, lun_id) = self._find_mapped_lun_igroup(path, initiator) - if lun_id is not None: - return lun_id - else: - raise exc_info[0], exc_info[1], exc_info[2] - - def _unmap_lun(self, path, initiator): - """Unmaps a lun from given initiator.""" - (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path, initiator) - self.zapi_client.unmap_lun(path, igroup_name) - - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped lun with initiator.""" - raise NotImplementedError() - - def _get_or_create_igroup(self, initiator, initiator_type='iscsi', - os='default'): - """Checks for an igroup for an initiator. - - Creates igroup if not found. - """ - - igroups = self.zapi_client.get_igroup_by_initiator(initiator=initiator) - igroup_name = None - for igroup in igroups: - if igroup['initiator-group-os-type'] == os: - if igroup['initiator-group-type'] == initiator_type or \ - igroup['initiator-group-type'] == 'mixed': - if igroup['initiator-group-name'].startswith( - self.IGROUP_PREFIX): - igroup_name = igroup['initiator-group-name'] - break - if not igroup_name: - igroup_name = self.IGROUP_PREFIX + six.text_type(uuid.uuid4()) - self.zapi_client.create_igroup(igroup_name, initiator_type, os) - self.zapi_client.add_igroup_initiator(igroup_name, initiator) - return igroup_name - - def _check_allowed_os(self, os): - """Checks if the os type supplied is NetApp supported.""" - if os in ['linux', 'aix', 'hpux', 'windows', 'solaris', - 'netware', 'vmware', 'openvms', 'xen', 'hyper_v']: - return True - else: - return False - - def _add_lun_to_table(self, lun): - """Adds LUN to cache table.""" - if not isinstance(lun, NetAppLun): - msg = _("Object is not a NetApp LUN.") - raise exception.VolumeBackendAPIException(data=msg) - self.lun_table[lun.name] = lun - - def _get_lun_from_table(self, name): - """Gets LUN from cache table. - - Refreshes cache if lun not found in cache. - """ - lun = self.lun_table.get(name) - if lun is None: - lun_list = self.zapi_client.get_lun_list() - self._extract_and_populate_luns(lun_list) - lun = self.lun_table.get(name) - if lun is None: - raise exception.VolumeNotFound(volume_id=name) - return lun - - def _clone_lun(self, name, new_name, space_reserved='true', - src_block=0, dest_block=0, block_count=0): - """Clone LUN with the given name to the new name.""" - raise NotImplementedError() - - def _get_lun_attr(self, name, attr): - """Get the lun attribute if found else None.""" - try: - attr = getattr(self._get_lun_from_table(name), attr) - return attr - except exception.VolumeNotFound as e: - LOG.error(_LE("Message: %s"), e.msg) - except Exception as e: - LOG.error(_LE("Error getting lun attribute. Exception: %s"), - e.__str__()) - return None - - def _create_lun_meta(self, lun): - raise NotImplementedError() - - def create_cloned_volume(self, volume, src_vref): - """Creates a clone of the specified volume.""" - vol_size = volume['size'] - src_vol = self._get_lun_from_table(src_vref['name']) - src_vol_size = src_vref['size'] - new_name = volume['name'] - self._clone_lun(src_vol.name, new_name, 'true') - if vol_size != src_vol_size: - try: - self.extend_volume(volume, volume['size']) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error(_LE("Resizing %s failed. " - "Cleaning volume."), new_name) - self.delete_volume(volume) - - def get_volume_stats(self, refresh=False): - """Get volume stats. - - If 'refresh' is True, run update the stats first. - """ - - if refresh: - self._update_volume_stats() - - return self._stats - - def _update_volume_stats(self): - """Retrieve stats info from volume group.""" - raise NotImplementedError() - - def extend_volume(self, volume, new_size): - """Extend an existing volume to the new size.""" - name = volume['name'] - lun = self._get_lun_from_table(name) - path = lun.metadata['Path'] - curr_size_bytes = six.text_type(lun.size) - new_size_bytes = six.text_type(int(new_size) * units.Gi) - # Reused by clone scenarios. - # Hence comparing the stored size. - if curr_size_bytes != new_size_bytes: - lun_geometry = self.zapi_client.get_lun_geometry(path) - if (lun_geometry and lun_geometry.get("max_resize") - and int(lun_geometry.get("max_resize")) >= - int(new_size_bytes)): - self.zapi_client.do_direct_resize(path, new_size_bytes) - else: - self._do_sub_clone_resize(path, new_size_bytes) - self.lun_table[name].size = new_size_bytes - else: - LOG.info(_LI("No need to extend volume %s" - " as it is already the requested new size."), name) - - def _get_vol_option(self, volume_name, option_name): - """Get the value for the volume option.""" - value = None - options = self.zapi_client.get_volume_options(volume_name) - for opt in options: - if opt.get_child_content('name') == option_name: - value = opt.get_child_content('value') - break - return value - - def _do_sub_clone_resize(self, path, new_size_bytes): - """Does sub lun clone after verification. - - Clones the block ranges and swaps - the luns also deletes older lun - after a successful clone. - """ - seg = path.split("/") - LOG.info(_LI("Resizing lun %s using sub clone to new size."), seg[-1]) - name = seg[-1] - vol_name = seg[2] - lun = self._get_lun_from_table(name) - metadata = lun.metadata - compression = self._get_vol_option(vol_name, 'compression') - if compression == "on": - msg = _('%s cannot be sub clone resized' - ' as it is hosted on compressed volume') - raise exception.VolumeBackendAPIException(data=msg % name) - else: - block_count = self._get_lun_block_count(path) - if block_count == 0: - msg = _('%s cannot be sub clone resized' - ' as it contains no blocks.') - raise exception.VolumeBackendAPIException(data=msg % name) - new_lun = 'new-%s' % (name) - self.zapi_client.create_lun(vol_name, new_lun, new_size_bytes, - metadata) - try: - self._clone_lun(name, new_lun, block_count=block_count) - self._post_sub_clone_resize(path) - except Exception: - with excutils.save_and_reraise_exception(): - new_path = '/vol/%s/%s' % (vol_name, new_lun) - self.zapi_client.destroy_lun(new_path) - - def _post_sub_clone_resize(self, path): - """Try post sub clone resize in a transactional manner.""" - st_tm_mv, st_nw_mv, st_del_old = None, None, None - seg = path.split("/") - LOG.info(_LI("Post clone resize lun %s"), seg[-1]) - new_lun = 'new-%s' % (seg[-1]) - tmp_lun = 'tmp-%s' % (seg[-1]) - tmp_path = "/vol/%s/%s" % (seg[2], tmp_lun) - new_path = "/vol/%s/%s" % (seg[2], new_lun) - try: - st_tm_mv = self.zapi_client.move_lun(path, tmp_path) - st_nw_mv = self.zapi_client.move_lun(new_path, path) - st_del_old = self.zapi_client.destroy_lun(tmp_path) - except Exception as e: - if st_tm_mv is None: - msg = _("Failure staging lun %s to tmp.") - raise exception.VolumeBackendAPIException(data=msg % (seg[-1])) - else: - if st_nw_mv is None: - self.zapi_client.move_lun(tmp_path, path) - msg = _("Failure moving new cloned lun to %s.") - raise exception.VolumeBackendAPIException( - data=msg % (seg[-1])) - elif st_del_old is None: - LOG.error(_LE("Failure deleting staged tmp lun %s."), - tmp_lun) - else: - LOG.error(_LE("Unknown exception in" - " post clone resize lun %s."), seg[-1]) - LOG.error(_LE("Exception details: %s") % (e.__str__())) - - def _get_lun_block_count(self, path): - """Gets block counts for the lun.""" - LOG.debug("Getting lun block count.") - block_count = 0 - lun_infos = self.zapi_client.get_lun_by_args(path=path) - if not lun_infos: - seg = path.split('/') - msg = _('Failure getting lun info for %s.') - raise exception.VolumeBackendAPIException(data=msg % seg[-1]) - lun_info = lun_infos[-1] - bs = int(lun_info.get_child_content('block-size')) - ls = int(lun_info.get_child_content('size')) - block_count = ls / bs - return block_count - - -class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver): - """NetApp C-mode iSCSI volume driver.""" - - DEFAULT_VS = 'openstack' - - def __init__(self, *args, **kwargs): - super(NetAppDirectCmodeISCSIDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(netapp_cluster_opts) - - def _do_custom_setup(self): - """Does custom setup for ontap cluster.""" - self.vserver = self.configuration.netapp_vserver - self.vserver = self.vserver if self.vserver else self.DEFAULT_VS - self.zapi_client = cmode.Client(self.client, self.vserver) - # We set vserver in client permanently. - # To use tunneling enable_tunneling while invoking api - self.client.set_vserver(self.vserver) - # Default values to run first api - self.client.set_api_version(1, 15) - (major, minor) = self.zapi_client.get_ontapi_version() - self.client.set_api_version(major, minor) - self.ssc_vols = None - self.stale_vols = set() - - def check_for_setup_error(self): - """Check that the driver is working and can communicate.""" - ssc_utils.check_ssc_api_permissions(self.client) - super(NetAppDirectCmodeISCSIDriver, self).check_for_setup_error() - - def create_lun(self, volume_name, lun_name, size, - metadata, qos_policy_group=None): - """Creates a LUN, handling ONTAP differences as needed.""" - - self.zapi_client.create_lun( - volume_name, lun_name, size, metadata, qos_policy_group) - - self._update_stale_vols( - volume=ssc_utils.NetAppVolume(volume_name, self.vserver)) - - def _create_lun_handle(self, metadata): - """Returns lun handle based on filer type.""" - return '%s:%s' % (self.vserver, metadata['Path']) - - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped lun with initiator.""" - initiator_igroups = self.zapi_client.get_igroup_by_initiator( - initiator=initiator) - lun_maps = self.zapi_client.get_lun_map(path) - if initiator_igroups and lun_maps: - for igroup in initiator_igroups: - igroup_name = igroup['initiator-group-name'] - if igroup_name.startswith(self.IGROUP_PREFIX): - for lun_map in lun_maps: - if lun_map['initiator-group'] == igroup_name: - return (igroup_name, lun_map['lun-id']) - return (None, None) - - def _clone_lun(self, name, new_name, space_reserved='true', - src_block=0, dest_block=0, block_count=0): - """Clone LUN with the given handle to the new name.""" - metadata = self._get_lun_attr(name, 'metadata') - volume = metadata['Volume'] - self.zapi_client.clone_lun(volume, name, new_name, space_reserved, - src_block=0, dest_block=0, block_count=0) - LOG.debug("Cloned LUN with new name %s" % new_name) - lun = self.zapi_client.get_lun_by_args(vserver=self.vserver, - path='/vol/%s/%s' - % (volume, new_name)) - if len(lun) == 0: - msg = _("No cloned lun named %s found on the filer") - raise exception.VolumeBackendAPIException(data=msg % (new_name)) - clone_meta = self._create_lun_meta(lun[0]) - self._add_lun_to_table(NetAppLun('%s:%s' % (clone_meta['Vserver'], - clone_meta['Path']), - new_name, - lun[0].get_child_content('size'), - clone_meta)) - self._update_stale_vols( - volume=ssc_utils.NetAppVolume(volume, self.vserver)) - - def _create_lun_meta(self, lun): - """Creates lun metadata dictionary.""" - self._is_naelement(lun) - meta_dict = {} - meta_dict['Vserver'] = lun.get_child_content('vserver') - meta_dict['Volume'] = lun.get_child_content('volume') - meta_dict['Qtree'] = lun.get_child_content('qtree') - meta_dict['Path'] = lun.get_child_content('path') - meta_dict['OsType'] = lun.get_child_content('multiprotocol-type') - meta_dict['SpaceReserved'] = \ - lun.get_child_content('is-space-reservation-enabled') - return meta_dict - - def _configure_tunneling(self, do_tunneling=False): - """Configures tunneling for ontap cluster.""" - if do_tunneling: - self.client.set_vserver(self.vserver) - else: - self.client.set_vserver(None) - - def _update_volume_stats(self): - """Retrieve stats info from vserver.""" - - sync = True if self.ssc_vols is None else False - ssc_utils.refresh_cluster_ssc(self, self.client, - self.vserver, synchronous=sync) - - LOG.debug('Updating volume stats') - data = {} - netapp_backend = 'NetApp_iSCSI_Cluster_direct' - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or netapp_backend - data['vendor_name'] = 'NetApp' - data['driver_version'] = self.VERSION - data['storage_protocol'] = 'iSCSI' - data['pools'] = self._get_pool_stats() - - na_utils.provide_ems(self, self.client, netapp_backend, - self._app_version) - self._stats = data - - def _get_pool_stats(self): - """Retrieve pool (i.e. ONTAP volume) stats info from SSC volumes.""" - - pools = [] - if not self.ssc_vols: - return pools - - for vol in self.ssc_vols['all']: - pool = dict() - pool['pool_name'] = vol.id['name'] - pool['QoS_support'] = False - pool['reserved_percentage'] = 0 - - # convert sizes to GB and de-rate by NetApp multiplier - total = float(vol.space['size_total_bytes']) - total /= self.configuration.netapp_size_multiplier - total /= units.Gi - pool['total_capacity_gb'] = round_down(total, '0.01') - - free = float(vol.space['size_avl_bytes']) - free /= self.configuration.netapp_size_multiplier - free /= units.Gi - pool['free_capacity_gb'] = round_down(free, '0.01') - - pool['netapp_raid_type'] = vol.aggr['raid_type'] - pool['netapp_disk_type'] = vol.aggr['disk_type'] - - mirrored = vol in self.ssc_vols['mirrored'] - pool['netapp_mirrored'] = six.text_type(mirrored).lower() - pool['netapp_unmirrored'] = six.text_type(not mirrored).lower() - - dedup = vol in self.ssc_vols['dedup'] - pool['netapp_dedup'] = six.text_type(dedup).lower() - pool['netapp_nodedup'] = six.text_type(not dedup).lower() - - compression = vol in self.ssc_vols['compression'] - pool['netapp_compression'] = six.text_type(compression).lower() - pool['netapp_nocompression'] = six.text_type( - not compression).lower() - - thin = vol in self.ssc_vols['thin'] - pool['netapp_thin_provisioned'] = six.text_type(thin).lower() - pool['netapp_thick_provisioned'] = six.text_type(not thin).lower() - - pools.append(pool) - - return pools - - @utils.synchronized('update_stale') - def _update_stale_vols(self, volume=None, reset=False): - """Populates stale vols with vol and returns set copy if reset.""" - if volume: - self.stale_vols.add(volume) - if reset: - set_copy = copy.deepcopy(self.stale_vols) - self.stale_vols.clear() - return set_copy - - @utils.synchronized("refresh_ssc_vols") - def refresh_ssc_vols(self, vols): - """Refreshes ssc_vols with latest entries.""" - self.ssc_vols = vols - - def delete_volume(self, volume): - """Driver entry point for destroying existing volumes.""" - try: - lun = self._get_lun_from_table(volume['name']) - except exception.VolumeNotFound: - lun = None - netapp_vol = None - if lun: - netapp_vol = lun.get_metadata_property('Volume') - super(NetAppDirectCmodeISCSIDriver, self).delete_volume(volume) - if netapp_vol: - self._update_stale_vols( - volume=ssc_utils.NetAppVolume(netapp_vol, self.vserver)) - - -class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver): - """NetApp 7-mode iSCSI volume driver.""" - - def __init__(self, *args, **kwargs): - super(NetAppDirect7modeISCSIDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(netapp_7mode_opts) - - def _do_custom_setup(self): - """Does custom setup depending on the type of filer.""" - self.vfiler = self.configuration.netapp_vfiler - self.volume_list = self.configuration.netapp_volume_list - if self.volume_list: - self.volume_list = self.volume_list.split(',') - self.volume_list = [el.strip() for el in self.volume_list] - self.zapi_client = seven_mode.Client(self.client, self.volume_list) - (major, minor) = self.zapi_client.get_ontapi_version() - self.client.set_api_version(major, minor) - if self.vfiler: - self.client.set_vfiler(self.vfiler) - self.vol_refresh_time = None - self.vol_refresh_interval = 1800 - self.vol_refresh_running = False - self.vol_refresh_voluntary = False - self.root_volume_name = self._get_root_volume_name() - - def check_for_setup_error(self): - """Check that the driver is working and can communicate.""" - api_version = self.client.get_api_version() - if api_version: - major, minor = api_version - if major == 1 and minor < 9: - msg = _("Unsupported ONTAP version." - " ONTAP version 7.3.1 and above is supported.") - raise exception.VolumeBackendAPIException(data=msg) - else: - msg = _("Api version could not be determined.") - raise exception.VolumeBackendAPIException(data=msg) - super(NetAppDirect7modeISCSIDriver, self).check_for_setup_error() - - def create_lun(self, volume_name, lun_name, size, - metadata, qos_policy_group=None): - """Creates a LUN, handling ONTAP differences as needed.""" - - self.zapi_client.create_lun( - volume_name, lun_name, size, metadata, qos_policy_group) - - self.vol_refresh_voluntary = True - - def _get_root_volume_name(self): - # switch to volume-get-root-name API when possible - vols = self.zapi_client.get_filer_volumes() - for vol in vols: - volume_name = vol.get_child_content('name') - if self._get_vol_option(volume_name, 'root') == 'true': - return volume_name - LOG.warning(_LW('Could not determine root volume name ' - 'on %s.') % self._get_owner()) - return None - - def _get_owner(self): - if self.vfiler: - owner = '%s:%s' % (self.configuration.netapp_server_hostname, - self.vfiler) - else: - owner = self.configuration.netapp_server_hostname - return owner - - def _create_lun_handle(self, metadata): - """Returns lun handle based on filer type.""" - owner = self._get_owner() - return '%s:%s' % (owner, metadata['Path']) - - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped lun with initiator.""" - result = self.zapi_client.get_lun_map(path) - igroups = result.get_child_by_name('initiator-groups') - if igroups: - igroup = None - lun_id = None - found = False - igroup_infs = igroups.get_children() - for ig in igroup_infs: - initiators = ig.get_child_by_name('initiators') - init_infs = initiators.get_children() - for info in init_infs: - if info.get_child_content('initiator-name') == initiator: - found = True - igroup = ig.get_child_content('initiator-group-name') - lun_id = ig.get_child_content('lun-id') - break - if found: - break - return (igroup, lun_id) - - def _clone_lun(self, name, new_name, space_reserved='true', - src_block=0, dest_block=0, block_count=0): - """Clone LUN with the given handle to the new name.""" - metadata = self._get_lun_attr(name, 'metadata') - path = metadata['Path'] - (parent, _splitter, name) = path.rpartition('/') - clone_path = '%s/%s' % (parent, new_name) - - self.zapi_client.clone_lun(path, clone_path, name, new_name, - space_reserved, src_block=0, - dest_block=0, block_count=0) - - self.vol_refresh_voluntary = True - luns = self.zapi_client.get_lun_by_args(path=clone_path) - if luns: - cloned_lun = luns[0] - self.zapi_client.set_space_reserve(clone_path, space_reserved) - clone_meta = self._create_lun_meta(cloned_lun) - handle = self._create_lun_handle(clone_meta) - self._add_lun_to_table( - NetAppLun(handle, new_name, - cloned_lun.get_child_content('size'), - clone_meta)) - else: - raise NaApiError('ENOLUNENTRY', 'No Lun entry found on the filer') - - def _create_lun_meta(self, lun): - """Creates lun metadata dictionary.""" - self._is_naelement(lun) - meta_dict = {} - meta_dict['Path'] = lun.get_child_content('path') - meta_dict['Volume'] = lun.get_child_content('path').split('/')[2] - meta_dict['OsType'] = lun.get_child_content('multiprotocol-type') - meta_dict['SpaceReserved'] = lun.get_child_content( - 'is-space-reservation-enabled') - return meta_dict - - def _update_volume_stats(self): - """Retrieve stats info from filer.""" - - # ensure we get current data - self.vol_refresh_voluntary = True - self._refresh_volume_info() - - LOG.debug('Updating volume stats') - data = {} - netapp_backend = 'NetApp_iSCSI_7mode_direct' - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or netapp_backend - data['vendor_name'] = 'NetApp' - data['driver_version'] = self.VERSION - data['storage_protocol'] = 'iSCSI' - data['pools'] = self._get_pool_stats() - - na_utils.provide_ems(self, self.client, netapp_backend, - self._app_version, server_type='7mode') - self._stats = data - - def _get_pool_stats(self): - """Retrieve pool (i.e. ONTAP volume) stats info from volumes.""" - - pools = [] - if not self.vols: - return pools - - for vol in self.vols: - - # omit volumes not specified in the config - volume_name = vol.get_child_content('name') - if self.volume_list and volume_name not in self.volume_list: - continue - - # omit root volume - if volume_name == self.root_volume_name: - continue - - # ensure good volume state - state = vol.get_child_content('state') - inconsistent = vol.get_child_content('is-inconsistent') - invalid = vol.get_child_content('is-invalid') - if (state != 'online' or - inconsistent != 'false' or - invalid != 'false'): - continue - - pool = dict() - pool['pool_name'] = volume_name - pool['QoS_support'] = False - pool['reserved_percentage'] = 0 - - # convert sizes to GB and de-rate by NetApp multiplier - total = float(vol.get_child_content('size-total') or 0) - total /= self.configuration.netapp_size_multiplier - total /= units.Gi - pool['total_capacity_gb'] = round_down(total, '0.01') - - free = float(vol.get_child_content('size-available') or 0) - free /= self.configuration.netapp_size_multiplier - free /= units.Gi - pool['free_capacity_gb'] = round_down(free, '0.01') - - pools.append(pool) - - return pools - - def _get_lun_block_count(self, path): - """Gets block counts for the lun.""" - bs = super( - NetAppDirect7modeISCSIDriver, self)._get_lun_block_count(path) - api_version = self.client.get_api_version() - if api_version: - major = api_version[0] - minor = api_version[1] - if major == 1 and minor < 15: - bs = bs - 1 - return bs - - def _refresh_volume_info(self): - """Saves the volume information for the filer.""" - - if (self.vol_refresh_time is None or self.vol_refresh_voluntary or - timeutils.is_newer_than(self.vol_refresh_time, - self.vol_refresh_interval)): - try: - job_set = set_safe_attr(self, 'vol_refresh_running', True) - if not job_set: - LOG.warning(_LW("Volume refresh job already " - "running. Returning...")) - return - self.vol_refresh_voluntary = False - self.vols = self.zapi_client.get_filer_volumes() - self.vol_refresh_time = timeutils.utcnow() - except Exception as e: - LOG.warning(_LW("Error refreshing volume info. Message: %s"), - six.text_type(e)) - finally: - set_safe_attr(self, 'vol_refresh_running', False) - - def delete_volume(self, volume): - """Driver entry point for destroying existing volumes.""" - super(NetAppDirect7modeISCSIDriver, self).delete_volume(volume) - self.vol_refresh_voluntary = True diff --git a/cinder/volume/drivers/netapp/nfs.py b/cinder/volume/drivers/netapp/nfs.py deleted file mode 100644 index da45edd9675..00000000000 --- a/cinder/volume/drivers/netapp/nfs.py +++ /dev/null @@ -1,1419 +0,0 @@ -# Copyright (c) 2012 NetApp, Inc. -# 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 -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Volume driver for NetApp NFS storage. -""" - -import os -import re -from threading import Timer -import time -import uuid - -from oslo.concurrency import processutils -from oslo.utils import excutils -from oslo.utils import units -import six -import six.moves.urllib.parse as urlparse - -from cinder import exception -from cinder.i18n import _, _LE, _LI, _LW -from cinder.image import image_utils -from cinder.openstack.common import log as logging -from cinder import utils -from cinder.volume.drivers.netapp.api import NaElement -from cinder.volume.drivers.netapp.api import NaServer -from cinder.volume.drivers.netapp.client import cmode -from cinder.volume.drivers.netapp.client import seven_mode -from cinder.volume.drivers.netapp.options import netapp_basicauth_opts -from cinder.volume.drivers.netapp.options import netapp_cluster_opts -from cinder.volume.drivers.netapp.options import netapp_connection_opts -from cinder.volume.drivers.netapp.options import netapp_img_cache_opts -from cinder.volume.drivers.netapp.options import netapp_nfs_extra_opts -from cinder.volume.drivers.netapp.options import netapp_transport_opts -from cinder.volume.drivers.netapp import ssc_utils -from cinder.volume.drivers.netapp import utils as na_utils -from cinder.volume.drivers.netapp.utils import get_volume_extra_specs -from cinder.volume.drivers.netapp.utils import validate_instantiation -from cinder.volume.drivers import nfs -from cinder.volume import utils as volume_utils - - -LOG = logging.getLogger(__name__) - - -class NetAppNFSDriver(nfs.NfsDriver): - """Base class for NetApp NFS driver. - Executes commands relating to Volumes. - """ - - # do not increment this as it may be used in volume type definitions - VERSION = "1.0.0" - - def __init__(self, *args, **kwargs): - # NOTE(vish): db is set by Manager - validate_instantiation(**kwargs) - self._execute = None - self._context = None - self._app_version = kwargs.pop("app_version", "unknown") - super(NetAppNFSDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(netapp_connection_opts) - self.configuration.append_config_values(netapp_basicauth_opts) - self.configuration.append_config_values(netapp_transport_opts) - self.configuration.append_config_values(netapp_img_cache_opts) - - def set_execute(self, execute): - self._execute = execute - - def do_setup(self, context): - super(NetAppNFSDriver, self).do_setup(context) - - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met.""" - raise NotImplementedError() - - def get_pool(self, volume): - """Return pool name where volume resides. - - :param volume: The volume hosted by the driver. - :return: Name of the pool where given volume is hosted. - """ - return volume['provider_location'] - - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - vol_size = volume.size - snap_size = snapshot.volume_size - - self._clone_volume(snapshot.name, volume.name, snapshot.volume_id) - share = self._get_volume_location(snapshot.volume_id) - volume['provider_location'] = share - path = self.local_path(volume) - run_as_root = self._execute_as_root - - if self._discover_file_till_timeout(path): - self._set_rw_permissions(path) - if vol_size != snap_size: - try: - self.extend_volume(volume, vol_size) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error(_LE("Resizing %s failed. Cleaning volume."), - volume.name) - self._execute('rm', path, run_as_root=run_as_root) - else: - raise exception.CinderException( - _("NFS file %s not discovered.") % volume['name']) - - return {'provider_location': volume['provider_location']} - - def create_snapshot(self, snapshot): - """Creates a snapshot.""" - self._clone_volume(snapshot['volume_name'], - snapshot['name'], - snapshot['volume_id']) - - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - nfs_mount = self._get_provider_location(snapshot.volume_id) - - if self._volume_not_present(nfs_mount, snapshot.name): - return True - - self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name), - run_as_root=self._execute_as_root) - - def _get_client(self): - """Creates client for server.""" - raise NotImplementedError() - - def _get_volume_location(self, volume_id): - """Returns NFS mount address as :.""" - nfs_server_ip = self._get_host_ip(volume_id) - export_path = self._get_export_path(volume_id) - return (nfs_server_ip + ':' + export_path) - - def _clone_volume(self, volume_name, clone_name, volume_id, share=None): - """Clones mounted volume using NetApp api.""" - raise NotImplementedError() - - def _get_provider_location(self, volume_id): - """Returns provider location for given volume.""" - volume = self.db.volume_get(self._context, volume_id) - return volume.provider_location - - def _get_host_ip(self, volume_id): - """Returns IP address for the given volume.""" - return self._get_provider_location(volume_id).split(':')[0] - - def _get_export_path(self, volume_id): - """Returns NFS export path for the given volume.""" - return self._get_provider_location(volume_id).split(':')[1] - - def _volume_not_present(self, nfs_mount, volume_name): - """Check if volume exists.""" - try: - self._try_execute('ls', self._get_volume_path(nfs_mount, - volume_name)) - except processutils.ProcessExecutionError: - # If the volume isn't present - return True - return False - - def _try_execute(self, *command, **kwargs): - # NOTE(vish): Volume commands can partially fail due to timing, but - # running them a second time on failure will usually - # recover nicely. - tries = 0 - while True: - try: - self._execute(*command, **kwargs) - return True - except processutils.ProcessExecutionError: - tries = tries + 1 - if tries >= self.configuration.num_shell_tries: - raise - LOG.exception(_LE("Recovering from a failed execute. " - "Try number %s"), tries) - time.sleep(tries ** 2) - - def _get_volume_path(self, nfs_share, volume_name): - """Get volume path (local fs path) for given volume name on given nfs - share. - - @param nfs_share string, example 172.18.194.100:/var/nfs - @param volume_name string, - example volume-91ee65ec-c473-4391-8c09-162b00c68a8c - """ - - return os.path.join(self._get_mount_point_for_share(nfs_share), - volume_name) - - def create_cloned_volume(self, volume, src_vref): - """Creates a clone of the specified volume.""" - vol_size = volume.size - src_vol_size = src_vref.size - self._clone_volume(src_vref.name, volume.name, src_vref.id) - share = self._get_volume_location(src_vref.id) - volume['provider_location'] = share - path = self.local_path(volume) - - if self._discover_file_till_timeout(path): - self._set_rw_permissions(path) - if vol_size != src_vol_size: - try: - self.extend_volume(volume, vol_size) - except Exception as e: - LOG.error(_LE("Resizing %s failed. " - "Cleaning volume. "), volume.name) - self._execute('rm', path, - run_as_root=self._execute_as_root) - raise e - else: - raise exception.CinderException( - _("NFS file %s not discovered.") % volume['name']) - - return {'provider_location': volume['provider_location']} - - def _update_volume_stats(self): - """Retrieve stats info from volume group.""" - raise NotImplementedError() - - def copy_image_to_volume(self, context, volume, image_service, image_id): - """Fetch the image from image_service and write it to the volume.""" - super(NetAppNFSDriver, self).copy_image_to_volume( - context, volume, image_service, image_id) - LOG.info(_LI('Copied image to volume %s using regular download.'), - volume['name']) - self._register_image_in_cache(volume, image_id) - - def _register_image_in_cache(self, volume, image_id): - """Stores image in the cache.""" - file_name = 'img-cache-%s' % image_id - LOG.info(_LI("Registering image in cache %s"), file_name) - try: - self._do_clone_rel_img_cache( - volume['name'], file_name, - volume['provider_location'], file_name) - except Exception as e: - LOG.warning(_LW('Exception while registering image %(image_id)s' - ' in cache. Exception: %(exc)s') - % {'image_id': image_id, 'exc': e.__str__()}) - - def _find_image_in_cache(self, image_id): - """Finds image in cache and returns list of shares with file name.""" - result = [] - if getattr(self, '_mounted_shares', None): - for share in self._mounted_shares: - dir = self._get_mount_point_for_share(share) - file_name = 'img-cache-%s' % image_id - file_path = '%s/%s' % (dir, file_name) - if os.path.exists(file_path): - LOG.debug('Found cache file for image %(image_id)s' - ' on share %(share)s' - % {'image_id': image_id, 'share': share}) - result.append((share, file_name)) - return result - - def _do_clone_rel_img_cache(self, src, dst, share, cache_file): - """Do clone operation w.r.t image cache file.""" - @utils.synchronized(cache_file, external=True) - def _do_clone(): - dir = self._get_mount_point_for_share(share) - file_path = '%s/%s' % (dir, dst) - if not os.path.exists(file_path): - LOG.info(_LI('Cloning from cache to destination %s'), dst) - self._clone_volume(src, dst, volume_id=None, share=share) - _do_clone() - - @utils.synchronized('clean_cache') - def _spawn_clean_cache_job(self): - """Spawns a clean task if not running.""" - if getattr(self, 'cleaning', None): - LOG.debug('Image cache cleaning in progress. Returning... ') - return - else: - # Set cleaning to True - self.cleaning = True - t = Timer(0, self._clean_image_cache) - t.start() - - def _clean_image_cache(self): - """Clean the image cache files in cache of space crunch.""" - try: - LOG.debug('Image cache cleaning in progress.') - thres_size_perc_start =\ - self.configuration.thres_avl_size_perc_start - thres_size_perc_stop = \ - self.configuration.thres_avl_size_perc_stop - for share in getattr(self, '_mounted_shares', []): - try: - total_size, total_avl, _total_alc = \ - self._get_capacity_info(share) - avl_percent = int((total_avl / total_size) * 100) - if avl_percent <= thres_size_perc_start: - LOG.info(_LI('Cleaning cache for share %s.'), share) - eligible_files = self._find_old_cache_files(share) - threshold_size = int( - (thres_size_perc_stop * total_size) / 100) - bytes_to_free = int(threshold_size - total_avl) - LOG.debug('Files to be queued for deletion %s', - eligible_files) - self._delete_files_till_bytes_free( - eligible_files, share, bytes_to_free) - else: - continue - except Exception as e: - LOG.warning(_LW('Exception during cache cleaning' - ' %(share)s. Message - %(ex)s') - % {'share': share, 'ex': e.__str__()}) - continue - finally: - LOG.debug('Image cache cleaning done.') - self.cleaning = False - - def _shortlist_del_eligible_files(self, share, old_files): - """Prepares list of eligible files to be deleted from cache.""" - raise NotImplementedError() - - def _find_old_cache_files(self, share): - """Finds the old files in cache.""" - mount_fs = self._get_mount_point_for_share(share) - threshold_minutes = self.configuration.expiry_thres_minutes - cmd = ['find', mount_fs, '-maxdepth', '1', '-name', - 'img-cache*', '-amin', '+%s' % (threshold_minutes)] - res, _err = self._execute(*cmd, - run_as_root=self._execute_as_root) - if res: - old_file_paths = res.strip('\n').split('\n') - mount_fs_len = len(mount_fs) - old_files = [x[mount_fs_len + 1:] for x in old_file_paths] - eligible_files = self._shortlist_del_eligible_files( - share, old_files) - return eligible_files - return [] - - def _delete_files_till_bytes_free(self, file_list, share, bytes_to_free=0): - """Delete files from disk till bytes are freed or list exhausted.""" - LOG.debug('Bytes to free %s', bytes_to_free) - if file_list and bytes_to_free > 0: - sorted_files = sorted(file_list, key=lambda x: x[1], reverse=True) - mount_fs = self._get_mount_point_for_share(share) - for f in sorted_files: - if f: - file_path = '%s/%s' % (mount_fs, f[0]) - LOG.debug('Delete file path %s', file_path) - - @utils.synchronized(f[0], external=True) - def _do_delete(): - if self._delete_file(file_path): - return True - return False - - if _do_delete(): - bytes_to_free = bytes_to_free - int(f[1]) - if bytes_to_free <= 0: - return - - def _delete_file(self, path): - """Delete file from disk and return result as boolean.""" - try: - LOG.debug('Deleting file at path %s', path) - cmd = ['rm', '-f', path] - self._execute(*cmd, run_as_root=self._execute_as_root) - return True - except Exception as ex: - LOG.warning(_LW('Exception during deleting %s'), ex.__str__()) - return False - - def clone_image(self, volume, image_location, image_id, image_meta): - """Create a volume efficiently from an existing image. - - image_location is a string whose format depends on the - image service backend in use. The driver should use it - to determine whether cloning is possible. - - image_id is a string which represents id of the image. - It can be used by the driver to introspect internal - stores or registry to do an efficient image clone. - - Returns a dict of volume properties eg. provider_location, - boolean indicating whether cloning occurred. - """ - - cloned = False - post_clone = False - share = None - try: - cache_result = self._find_image_in_cache(image_id) - if cache_result: - cloned = self._clone_from_cache(volume, image_id, cache_result) - else: - cloned = self._direct_nfs_clone(volume, image_location, - image_id) - if cloned: - post_clone = self._post_clone_image(volume) - except Exception as e: - msg = e.msg if getattr(e, 'msg', None) else e.__str__() - LOG.info(_LI('Image cloning unsuccessful for image' - ' %(image_id)s. Message: %(msg)s') - % {'image_id': image_id, 'msg': msg}) - vol_path = self.local_path(volume) - volume['provider_location'] = None - if os.path.exists(vol_path): - self._delete_file(vol_path) - finally: - cloned = cloned and post_clone - share = volume['provider_location'] if cloned else None - bootable = True if cloned else False - return {'provider_location': share, 'bootable': bootable}, cloned - - def _clone_from_cache(self, volume, image_id, cache_result): - """Clones a copy from image cache.""" - cloned = False - LOG.info(_LI('Cloning image %s from cache'), image_id) - for res in cache_result: - # Repeat tries in other shares if failed in some - (share, file_name) = res - LOG.debug('Cache share: %s', share) - if (share and - self._is_share_vol_compatible(volume, share)): - try: - self._do_clone_rel_img_cache( - file_name, volume['name'], share, file_name) - cloned = True - volume['provider_location'] = share - break - except Exception: - LOG.warning(_LW('Unexpected exception during' - ' image cloning in share %s'), share) - return cloned - - def _direct_nfs_clone(self, volume, image_location, image_id): - """Clone directly in nfs share.""" - LOG.info(_LI('Checking image clone %s from glance share.'), image_id) - cloned = False - image_location = self._construct_image_nfs_url(image_location) - share = self._is_cloneable_share(image_location) - run_as_root = self._execute_as_root - - if share and self._is_share_vol_compatible(volume, share): - LOG.debug('Share is cloneable %s', share) - volume['provider_location'] = share - (__, ___, img_file) = image_location.rpartition('/') - dir_path = self._get_mount_point_for_share(share) - img_path = '%s/%s' % (dir_path, img_file) - img_info = image_utils.qemu_img_info(img_path, - run_as_root=run_as_root) - if img_info.file_format == 'raw': - LOG.debug('Image is raw %s', image_id) - self._clone_volume( - img_file, volume['name'], - volume_id=None, share=share) - cloned = True - else: - LOG.info(_LI('Image will locally be converted to raw %s'), - image_id) - dst = '%s/%s' % (dir_path, volume['name']) - image_utils.convert_image(img_path, dst, 'raw', - run_as_root=run_as_root) - data = image_utils.qemu_img_info(dst, run_as_root=run_as_root) - if data.file_format != "raw": - raise exception.InvalidResults( - _("Converted to raw, but" - " format is now %s") % data.file_format) - else: - cloned = True - self._register_image_in_cache( - volume, image_id) - return cloned - - def _post_clone_image(self, volume): - """Do operations post image cloning.""" - LOG.info(_LI('Performing post clone for %s'), volume['name']) - vol_path = self.local_path(volume) - if self._discover_file_till_timeout(vol_path): - self._set_rw_permissions(vol_path) - self._resize_image_file(vol_path, volume['size']) - return True - raise exception.InvalidResults( - _("NFS file could not be discovered.")) - - def _resize_image_file(self, path, new_size): - """Resize the image file on share to new size.""" - LOG.debug('Checking file for resize') - if self._is_file_size_equal(path, new_size): - return - else: - LOG.info(_LI('Resizing file to %sG'), new_size) - image_utils.resize_image(path, new_size, - run_as_root=self._execute_as_root) - if self._is_file_size_equal(path, new_size): - return - else: - raise exception.InvalidResults( - _('Resizing image file failed.')) - - def _is_file_size_equal(self, path, size): - """Checks if file size at path is equal to size.""" - data = image_utils.qemu_img_info(path, - run_as_root=self._execute_as_root) - virt_size = data.virtual_size / units.Gi - if virt_size == size: - return True - else: - return False - - def _discover_file_till_timeout(self, path, timeout=45): - """Checks if file size at path is equal to size.""" - # Sometimes nfs takes time to discover file - # Retrying in case any unexpected situation occurs - retry_seconds = timeout - sleep_interval = 2 - while True: - if os.path.exists(path): - return True - else: - if retry_seconds <= 0: - LOG.warning(_LW('Discover file retries exhausted.')) - return False - else: - time.sleep(sleep_interval) - retry_seconds = retry_seconds - sleep_interval - - def _is_cloneable_share(self, image_location): - """Finds if the image at location is cloneable.""" - conn, dr = self._check_get_nfs_path_segs(image_location) - return self._check_share_in_use(conn, dr) - - def _check_get_nfs_path_segs(self, image_location): - """Checks if the nfs path format is matched. - - WebNFS url format with relative-path is supported. - Accepting all characters in path-names and checking - against the mounted shares which will contain only - allowed path segments. Returns connection and dir details. - """ - conn, dr = None, None - if image_location: - nfs_loc_pattern = \ - ('^nfs://(([\w\-\.]+:{1}[\d]+|[\w\-\.]+)(/[^\/].*)' - '*(/[^\/\\\\]+)$)') - matched = re.match(nfs_loc_pattern, image_location, flags=0) - if not matched: - LOG.debug('Image location not in the' - ' expected format %s', image_location) - else: - conn = matched.group(2) - dr = matched.group(3) or '/' - return (conn, dr) - - def _share_match_for_ip(self, ip, shares): - """Returns the share that is served by ip. - - Multiple shares can have same dir path but - can be served using different ips. It finds the - share which is served by ip on same nfs server. - """ - raise NotImplementedError() - - def _check_share_in_use(self, conn, dir): - """Checks if share is cinder mounted and returns it.""" - try: - if conn: - host = conn.split(':')[0] - ip = na_utils.resolve_hostname(host) - share_candidates = [] - for sh in self._mounted_shares: - sh_exp = sh.split(':')[1] - if sh_exp == dir: - share_candidates.append(sh) - if share_candidates: - LOG.debug('Found possible share matches %s', - share_candidates) - return self._share_match_for_ip(ip, share_candidates) - except Exception: - LOG.warning(_LW("Unexpected exception while short " - "listing used share.")) - return None - - def _construct_image_nfs_url(self, image_location): - """Construct direct url for nfs backend. - - It creates direct url from image_location - which is a tuple with direct_url and locations. - Returns url with nfs scheme if nfs store - else returns url. It needs to be verified - by backend before use. - """ - - direct_url, locations = image_location - if not direct_url and not locations: - raise exception.NotFound(_('Image location not present.')) - - # Locations will be always a list of one until - # bp multiple-image-locations is introduced - if not locations: - return direct_url - location = locations[0] - url = location['url'] - if not location['metadata']: - return url - location_type = location['metadata'].get('type') - if not location_type or location_type.lower() != "nfs": - return url - share_location = location['metadata'].get('share_location') - mount_point = location['metadata'].get('mount_point') - if not share_location or not mount_point: - return url - url_parse = urlparse.urlparse(url) - abs_path = os.path.join(url_parse.netloc, url_parse.path) - rel_path = os.path.relpath(abs_path, mount_point) - direct_url = "%s/%s" % (share_location, rel_path) - return direct_url - - def extend_volume(self, volume, new_size): - """Extend an existing volume to the new size.""" - LOG.info(_LI('Extending volume %s.'), volume['name']) - path = self.local_path(volume) - self._resize_image_file(path, new_size) - - def _is_share_vol_compatible(self, volume, share): - """Checks if share is compatible with volume to host it.""" - raise NotImplementedError() - - def _check_share_can_hold_size(self, share, size): - """Checks if volume can hold image with size.""" - _tot_size, tot_available, _tot_allocated = self._get_capacity_info( - share) - if tot_available < size: - msg = _("Container size smaller than required file size.") - raise exception.VolumeDriverException(msg) - - def _move_nfs_file(self, source_path, dest_path): - """Moves source to destination.""" - - @utils.synchronized(dest_path, external=True) - def _move_file(src, dst): - if os.path.exists(dst): - LOG.warning(_LW("Destination %s already exists."), dst) - return False - self._execute('mv', src, dst, - run_as_root=self._execute_as_root) - return True - - try: - return _move_file(source_path, dest_path) - except Exception as e: - LOG.warning(_LW('Exception moving file %(src)s. Message - %(e)s') - % {'src': source_path, 'e': e}) - return False - - -class NetAppDirectNfsDriver(NetAppNFSDriver): - """Executes commands related to volumes on NetApp filer.""" - - def __init__(self, *args, **kwargs): - super(NetAppDirectNfsDriver, self).__init__(*args, **kwargs) - - def do_setup(self, context): - super(NetAppDirectNfsDriver, self).do_setup(context) - self._context = context - self._client = self._get_client() - self._do_custom_setup(self._client) - - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met.""" - self._check_flags() - - def _check_flags(self): - """Raises error if any required configuration flag is missing.""" - required_flags = ['netapp_login', - 'netapp_password', - 'netapp_server_hostname'] - for flag in required_flags: - if not getattr(self.configuration, flag, None): - raise exception.CinderException(_('%s is not set') % flag) - - def _get_client(self): - """Creates NetApp api client.""" - client = NaServer( - host=self.configuration.netapp_server_hostname, - server_type=NaServer.SERVER_TYPE_FILER, - transport_type=self.configuration.netapp_transport_type, - style=NaServer.STYLE_LOGIN_PASSWORD, - username=self.configuration.netapp_login, - password=self.configuration.netapp_password) - if self.configuration.netapp_server_port is not None: - client.set_port(self.configuration.netapp_server_port) - return client - - def _do_custom_setup(self, client): - """Do the customized set up on client if any for different types.""" - raise NotImplementedError() - - def _is_naelement(self, elem): - """Checks if element is NetApp element.""" - if not isinstance(elem, NaElement): - raise ValueError('Expects NaElement') - - def _get_export_ip_path(self, volume_id=None, share=None): - """Returns export ip and path. - - One of volume id or share is used to return the values. - """ - - if volume_id: - host_ip = self._get_host_ip(volume_id) - export_path = self._get_export_path(volume_id) - elif share: - host_ip = share.split(':')[0] - export_path = share.split(':')[1] - else: - raise exception.InvalidInput('None of vol id or share specified.') - return (host_ip, export_path) - - def _create_file_usage_req(self, path): - """Creates the request element for file_usage_get.""" - file_use = NaElement.create_node_with_children( - 'file-usage-get', **{'path': path}) - return file_use - - def _get_extended_capacity_info(self, nfs_share): - """Returns an extended set of share capacity metrics.""" - - total_size, total_available, total_allocated = \ - self._get_capacity_info(nfs_share) - - used_ratio = (total_size - total_available) / total_size - subscribed_ratio = total_allocated / total_size - apparent_size = max(0, total_size * self.configuration.nfs_used_ratio) - apparent_available = max(0, apparent_size - total_allocated) - - return {'total_size': total_size, 'total_available': total_available, - 'total_allocated': total_allocated, 'used_ratio': used_ratio, - 'subscribed_ratio': subscribed_ratio, - 'apparent_size': apparent_size, - 'apparent_available': apparent_available} - - -class NetAppDirectCmodeNfsDriver(NetAppDirectNfsDriver): - """Executes commands related to volumes on c mode.""" - - def __init__(self, *args, **kwargs): - super(NetAppDirectCmodeNfsDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(netapp_cluster_opts) - self.configuration.append_config_values(netapp_nfs_extra_opts) - - def _do_custom_setup(self, client): - """Do the customized set up on client for cluster mode.""" - # Default values to run first api - client.set_api_version(1, 15) - self.vserver = self.configuration.netapp_vserver - self.zapi_client = cmode.Client(client, self.vserver) - (major, minor) = self.zapi_client.get_ontapi_version() - client.set_api_version(major, minor) - self.ssc_vols = None - self.stale_vols = set() - if self.vserver: - self.ssc_enabled = True - LOG.info(_LI("Shares on vserver %s will only" - " be used for provisioning.") % self.vserver) - else: - self.ssc_enabled = False - LOG.warning(_LW("No vserver set in config. " - "SSC will be disabled.")) - - def check_for_setup_error(self): - """Check that the driver is working and can communicate.""" - super(NetAppDirectCmodeNfsDriver, self).check_for_setup_error() - if self.ssc_enabled: - ssc_utils.check_ssc_api_permissions(self._client) - - def create_volume(self, volume): - """Creates a volume. - - :param volume: volume reference - """ - LOG.debug('create_volume on %s' % volume['host']) - self._ensure_shares_mounted() - - # get share as pool name - share = volume_utils.extract_host(volume['host'], level='pool') - - if share is None: - msg = _("Pool is not available in the volume host field.") - raise exception.InvalidHost(reason=msg) - - extra_specs = get_volume_extra_specs(volume) - qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ - if extra_specs else None - - # warn on obsolete extra specs - na_utils.log_extra_spec_warnings(extra_specs) - - try: - volume['provider_location'] = share - LOG.info(_LI('casted to %s') % volume['provider_location']) - self._do_create_volume(volume) - if qos_policy_group: - self._set_qos_policy_group_on_volume(volume, share, - qos_policy_group) - return {'provider_location': volume['provider_location']} - except Exception as ex: - LOG.error(_LE("Exception creating vol %(name)s on " - "share %(share)s. Details: %(ex)s") - % {'name': volume['name'], - 'share': volume['provider_location'], - 'ex': ex}) - volume['provider_location'] = None - finally: - if self.ssc_enabled: - self._update_stale_vols(self._get_vol_for_share(share)) - - msg = _("Volume %s could not be created on shares.") - raise exception.VolumeBackendAPIException(data=msg % (volume['name'])) - - def _set_qos_policy_group_on_volume(self, volume, share, qos_policy_group): - target_path = '%s' % (volume['name']) - export_path = share.split(':')[1] - flex_vol_name = self.zapi_client.get_vol_by_junc_vserver(self.vserver, - export_path) - self.zapi_client.file_assign_qos(flex_vol_name, - qos_policy_group, - target_path) - - def _clone_volume(self, volume_name, clone_name, - volume_id, share=None): - """Clones mounted volume on NetApp Cluster.""" - (vserver, exp_volume) = self._get_vserver_and_exp_vol(volume_id, share) - self.zapi_client.clone_file(exp_volume, volume_name, clone_name, - vserver) - share = share if share else self._get_provider_location(volume_id) - self._post_prov_deprov_in_ssc(share) - - def _get_vserver_and_exp_vol(self, volume_id=None, share=None): - """Gets the vserver and export volume for share.""" - (host_ip, export_path) = self._get_export_ip_path(volume_id, share) - ifs = self.zapi_client.get_if_info_by_ip(host_ip) - vserver = ifs[0].get_child_content('vserver') - exp_volume = self.zapi_client.get_vol_by_junc_vserver(vserver, - export_path) - return (vserver, exp_volume) - - def _get_vserver_ips(self, vserver): - """Get ips for the vserver.""" - result = na_utils.invoke_api( - self._client, api_name='net-interface-get-iter', - is_iter=True, tunnel=vserver) - if_list = [] - for res in result: - records = res.get_child_content('num-records') - if records > 0: - attr_list = res['attributes-list'] - ifs = attr_list.get_children() - if_list.extend(ifs) - return if_list - - def _update_volume_stats(self): - """Retrieve stats info from vserver.""" - - self._ensure_shares_mounted() - sync = True if self.ssc_vols is None else False - ssc_utils.refresh_cluster_ssc(self, self._client, - self.vserver, synchronous=sync) - - LOG.debug('Updating volume stats') - data = {} - netapp_backend = 'NetApp_NFS_Cluster_direct' - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or netapp_backend - data['vendor_name'] = 'NetApp' - data['driver_version'] = self.VERSION - data['storage_protocol'] = 'nfs' - data['pools'] = self._get_pool_stats() - - self._spawn_clean_cache_job() - na_utils.provide_ems(self, self._client, netapp_backend, - self._app_version) - self._stats = data - - def _get_pool_stats(self): - """Retrieve pool (i.e. NFS share) stats info from SSC volumes.""" - - pools = [] - - for nfs_share in self._mounted_shares: - - capacity = self._get_extended_capacity_info(nfs_share) - - pool = dict() - pool['pool_name'] = nfs_share - pool['QoS_support'] = False - pool['reserved_percentage'] = 0 - - # Report pool as reserved when over the configured used_ratio - if capacity['used_ratio'] > self.configuration.nfs_used_ratio: - pool['reserved_percentage'] = 100 - - # Report pool as reserved when over the subscribed ratio - if capacity['subscribed_ratio'] >=\ - self.configuration.nfs_oversub_ratio: - pool['reserved_percentage'] = 100 - - # convert sizes to GB - total = float(capacity['apparent_size']) / units.Gi - pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') - - free = float(capacity['apparent_available']) / units.Gi - pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') - - # add SSC content if available - vol = self._get_vol_for_share(nfs_share) - if vol and self.ssc_vols: - pool['netapp_raid_type'] = vol.aggr['raid_type'] - pool['netapp_disk_type'] = vol.aggr['disk_type'] - - mirrored = vol in self.ssc_vols['mirrored'] - pool['netapp_mirrored'] = six.text_type(mirrored).lower() - pool['netapp_unmirrored'] = six.text_type(not mirrored).lower() - - dedup = vol in self.ssc_vols['dedup'] - pool['netapp_dedup'] = six.text_type(dedup).lower() - pool['netapp_nodedup'] = six.text_type(not dedup).lower() - - compression = vol in self.ssc_vols['compression'] - pool['netapp_compression'] = six.text_type(compression).lower() - pool['netapp_nocompression'] = six.text_type( - not compression).lower() - - thin = vol in self.ssc_vols['thin'] - pool['netapp_thin_provisioned'] = six.text_type(thin).lower() - pool['netapp_thick_provisioned'] = six.text_type( - not thin).lower() - - pools.append(pool) - - return pools - - @utils.synchronized('update_stale') - def _update_stale_vols(self, volume=None, reset=False): - """Populates stale vols with vol and returns set copy.""" - if volume: - self.stale_vols.add(volume) - set_copy = self.stale_vols.copy() - if reset: - self.stale_vols.clear() - return set_copy - - @utils.synchronized("refresh_ssc_vols") - def refresh_ssc_vols(self, vols): - """Refreshes ssc_vols with latest entries.""" - if not self._mounted_shares: - LOG.warning(_LW("No shares found hence skipping ssc refresh.")) - return - mnt_share_vols = set() - vs_ifs = self._get_vserver_ips(self.vserver) - for vol in vols['all']: - for sh in self._mounted_shares: - host = sh.split(':')[0] - junction = sh.split(':')[1] - ip = na_utils.resolve_hostname(host) - if (self._ip_in_ifs(ip, vs_ifs) and - junction == vol.id['junction_path']): - mnt_share_vols.add(vol) - vol.export['path'] = sh - break - for key in vols.keys(): - vols[key] = vols[key] & mnt_share_vols - self.ssc_vols = vols - - def _ip_in_ifs(self, ip, api_ifs): - """Checks if ip is listed for ifs in api format.""" - if api_ifs is None: - return False - for ifc in api_ifs: - ifc_ip = ifc.get_child_content("address") - if ifc_ip == ip: - return True - return False - - def _shortlist_del_eligible_files(self, share, old_files): - """Prepares list of eligible files to be deleted from cache.""" - file_list = [] - (vserver, exp_volume) = self._get_vserver_and_exp_vol( - volume_id=None, share=share) - for file in old_files: - path = '/vol/%s/%s' % (exp_volume, file) - u_bytes = self.zapi_client.get_file_usage(path, vserver) - file_list.append((file, u_bytes)) - LOG.debug('Shortlisted del elg files %s', file_list) - return file_list - - def _share_match_for_ip(self, ip, shares): - """Returns the share that is served by ip. - - Multiple shares can have same dir path but - can be served using different ips. It finds the - share which is served by ip on same nfs server. - """ - ip_vserver = self._get_vserver_for_ip(ip) - if ip_vserver and shares: - for share in shares: - ip_sh = share.split(':')[0] - sh_vserver = self._get_vserver_for_ip(ip_sh) - if sh_vserver == ip_vserver: - LOG.debug('Share match found for ip %s', ip) - return share - LOG.debug('No share match found for ip %s', ip) - return None - - def _get_vserver_for_ip(self, ip): - """Get vserver for the mentioned ip.""" - try: - ifs = self.zapi_client.get_if_info_by_ip(ip) - vserver = ifs[0].get_child_content('vserver') - return vserver - except Exception: - return None - - def _get_vol_for_share(self, nfs_share): - """Gets the ssc vol with given share.""" - if self.ssc_vols: - for vol in self.ssc_vols['all']: - if vol.export['path'] == nfs_share: - return vol - return None - - def _is_share_vol_compatible(self, volume, share): - """Checks if share is compatible with volume to host it.""" - compatible = self._is_share_eligible(share, volume['size']) - if compatible and self.ssc_enabled: - matched = self._is_share_vol_type_match(volume, share) - compatible = compatible and matched - return compatible - - def _is_share_vol_type_match(self, volume, share): - """Checks if share matches volume type.""" - netapp_vol = self._get_vol_for_share(share) - LOG.debug("Found volume %(vol)s for share %(share)s." - % {'vol': netapp_vol, 'share': share}) - extra_specs = get_volume_extra_specs(volume) - vols = ssc_utils.get_volumes_for_specs(self.ssc_vols, extra_specs) - return netapp_vol in vols - - def delete_volume(self, volume): - """Deletes a logical volume.""" - share = volume['provider_location'] - super(NetAppDirectCmodeNfsDriver, self).delete_volume(volume) - self._post_prov_deprov_in_ssc(share) - - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - share = self._get_provider_location(snapshot.volume_id) - super(NetAppDirectCmodeNfsDriver, self).delete_snapshot(snapshot) - self._post_prov_deprov_in_ssc(share) - - def _post_prov_deprov_in_ssc(self, share): - if self.ssc_enabled and share: - netapp_vol = self._get_vol_for_share(share) - if netapp_vol: - self._update_stale_vols(volume=netapp_vol) - - def copy_image_to_volume(self, context, volume, image_service, image_id): - """Fetch the image from image_service and write it to the volume.""" - copy_success = False - try: - major, minor = self._client.get_api_version() - col_path = self.configuration.netapp_copyoffload_tool_path - if (major == 1 and minor >= 20 and col_path): - self._try_copyoffload(context, volume, image_service, image_id) - copy_success = True - LOG.info(_LI('Copied image %(img)s to ' - 'volume %(vol)s using copy' - ' offload workflow.') - % {'img': image_id, 'vol': volume['id']}) - else: - LOG.debug("Copy offload either not configured or" - " unsupported.") - except Exception as e: - LOG.exception(_LE('Copy offload workflow unsuccessful. %s'), e) - finally: - if not copy_success: - super(NetAppDirectCmodeNfsDriver, self).copy_image_to_volume( - context, volume, image_service, image_id) - if self.ssc_enabled: - sh = self._get_provider_location(volume['id']) - self._update_stale_vols(self._get_vol_for_share(sh)) - - def _try_copyoffload(self, context, volume, image_service, image_id): - """Tries server side file copy offload.""" - copied = False - cache_result = self._find_image_in_cache(image_id) - if cache_result: - copied = self._copy_from_cache(volume, image_id, cache_result) - if not cache_result or not copied: - self._copy_from_img_service(context, volume, image_service, - image_id) - - def _get_ip_verify_on_cluster(self, host): - """Verifies if host on same cluster and returns ip.""" - ip = na_utils.resolve_hostname(host) - vserver = self._get_vserver_for_ip(ip) - if not vserver: - raise exception.NotFound(_("No vserver owning the ip %s.") % ip) - return ip - - def _copy_from_cache(self, volume, image_id, cache_result): - """Try copying image file_name from cached file_name.""" - LOG.debug("Trying copy from cache using copy offload.") - copied = False - for res in cache_result: - try: - (share, file_name) = res - LOG.debug("Found cache file_name on share %s.", share) - if share != self._get_provider_location(volume['id']): - col_path = self.configuration.netapp_copyoffload_tool_path - src_ip = self._get_ip_verify_on_cluster( - share.split(':')[0]) - src_path = os.path.join(share.split(':')[1], file_name) - dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip( - volume['id'])) - dst_path = os.path.join( - self._get_export_path(volume['id']), volume['name']) - self._execute(col_path, src_ip, dst_ip, - src_path, dst_path, - run_as_root=self._execute_as_root, - check_exit_code=0) - self._register_image_in_cache(volume, image_id) - LOG.debug("Copied image from cache to volume %s using" - " copy offload.", volume['id']) - else: - self._clone_file_dst_exists(share, file_name, - volume['name'], - dest_exists=True) - LOG.debug("Copied image from cache to volume %s using" - " cloning.", volume['id']) - self._post_clone_image(volume) - copied = True - break - except Exception as e: - LOG.exception(_LE('Error in workflow copy ' - 'from cache. %s.'), e) - return copied - - def _clone_file_dst_exists(self, share, src_name, dst_name, - dest_exists=False): - """Clone file even if dest exists.""" - (vserver, exp_volume) = self._get_vserver_and_exp_vol(share=share) - self.zapi_client.clone_file(exp_volume, src_name, dst_name, vserver, - dest_exists=dest_exists) - - def _copy_from_img_service(self, context, volume, image_service, - image_id): - """Copies from the image service using copy offload.""" - LOG.debug("Trying copy from image service using copy offload.") - image_loc = image_service.get_location(context, image_id) - image_loc = self._construct_image_nfs_url(image_loc) - conn, dr = self._check_get_nfs_path_segs(image_loc) - if conn: - src_ip = self._get_ip_verify_on_cluster(conn.split(':')[0]) - else: - raise exception.NotFound(_("Source host details not found.")) - (__, ___, img_file) = image_loc.rpartition('/') - src_path = os.path.join(dr, img_file) - dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip( - volume['id'])) - # tmp file is required to deal with img formats - tmp_img_file = six.text_type(uuid.uuid4()) - col_path = self.configuration.netapp_copyoffload_tool_path - img_info = image_service.show(context, image_id) - dst_share = self._get_provider_location(volume['id']) - self._check_share_can_hold_size(dst_share, img_info['size']) - run_as_root = self._execute_as_root - - dst_dir = self._get_mount_point_for_share(dst_share) - dst_img_local = os.path.join(dst_dir, tmp_img_file) - try: - # If src and dst share not equal - if (('%s:%s' % (src_ip, dr)) != - ('%s:%s' % (dst_ip, self._get_export_path(volume['id'])))): - dst_img_serv_path = os.path.join( - self._get_export_path(volume['id']), tmp_img_file) - self._execute(col_path, src_ip, dst_ip, src_path, - dst_img_serv_path, run_as_root=run_as_root, - check_exit_code=0) - else: - self._clone_file_dst_exists(dst_share, img_file, tmp_img_file) - self._discover_file_till_timeout(dst_img_local, timeout=120) - LOG.debug('Copied image %(img)s to tmp file %(tmp)s.' - % {'img': image_id, 'tmp': tmp_img_file}) - dst_img_cache_local = os.path.join(dst_dir, - 'img-cache-%s' % (image_id)) - if img_info['disk_format'] == 'raw': - LOG.debug('Image is raw %s.', image_id) - self._clone_file_dst_exists(dst_share, tmp_img_file, - volume['name'], dest_exists=True) - self._move_nfs_file(dst_img_local, dst_img_cache_local) - LOG.debug('Copied raw image %(img)s to volume %(vol)s.' - % {'img': image_id, 'vol': volume['id']}) - else: - LOG.debug('Image will be converted to raw %s.', image_id) - img_conv = six.text_type(uuid.uuid4()) - dst_img_conv_local = os.path.join(dst_dir, img_conv) - - # Checking against image size which is approximate check - self._check_share_can_hold_size(dst_share, img_info['size']) - try: - image_utils.convert_image(dst_img_local, - dst_img_conv_local, 'raw', - run_as_root=run_as_root) - data = image_utils.qemu_img_info(dst_img_conv_local, - run_as_root=run_as_root) - if data.file_format != "raw": - raise exception.InvalidResults( - _("Converted to raw, but format is now %s.") - % data.file_format) - else: - self._clone_file_dst_exists(dst_share, img_conv, - volume['name'], - dest_exists=True) - self._move_nfs_file(dst_img_conv_local, - dst_img_cache_local) - LOG.debug('Copied locally converted raw image' - ' %(img)s to volume %(vol)s.' - % {'img': image_id, 'vol': volume['id']}) - finally: - if os.path.exists(dst_img_conv_local): - self._delete_file(dst_img_conv_local) - self._post_clone_image(volume) - finally: - if os.path.exists(dst_img_local): - self._delete_file(dst_img_local) - - -class NetAppDirect7modeNfsDriver(NetAppDirectNfsDriver): - """Executes commands related to volumes on 7 mode.""" - - def __init__(self, *args, **kwargs): - super(NetAppDirect7modeNfsDriver, self).__init__(*args, **kwargs) - - def _do_custom_setup(self, client): - """Do the customized set up on client if any for 7 mode.""" - self.zapi_client = seven_mode.Client(client) - (major, minor) = self.zapi_client.get_ontapi_version() - client.set_api_version(major, minor) - - def check_for_setup_error(self): - """Checks if setup occurred properly.""" - api_version = self._client.get_api_version() - if api_version: - major, minor = api_version - if major == 1 and minor < 9: - msg = _("Unsupported ONTAP version." - " ONTAP version 7.3.1 and above is supported.") - raise exception.VolumeBackendAPIException(data=msg) - else: - msg = _("Api version could not be determined.") - raise exception.VolumeBackendAPIException(data=msg) - super(NetAppDirect7modeNfsDriver, self).check_for_setup_error() - - def create_volume(self, volume): - """Creates a volume. - - :param volume: volume reference - """ - LOG.debug('create_volume on %s' % volume['host']) - self._ensure_shares_mounted() - - # get share as pool name - share = volume_utils.extract_host(volume['host'], level='pool') - - if share is None: - msg = _("Pool is not available in the volume host field.") - raise exception.InvalidHost(reason=msg) - - volume['provider_location'] = share - LOG.info(_LI('Creating volume at location %s') - % volume['provider_location']) - - try: - self._do_create_volume(volume) - except Exception as ex: - LOG.error(_LE("Exception creating vol %(name)s on " - "share %(share)s. Details: %(ex)s") - % {'name': volume['name'], - 'share': volume['provider_location'], - 'ex': six.text_type(ex)}) - msg = _("Volume %s could not be created on shares.") - raise exception.VolumeBackendAPIException( - data=msg % (volume['name'])) - - return {'provider_location': volume['provider_location']} - - def _clone_volume(self, volume_name, clone_name, - volume_id, share=None): - """Clones mounted volume with NetApp filer.""" - (_host_ip, export_path) = self._get_export_ip_path(volume_id, share) - storage_path = self.zapi_client.get_actual_path_for_export(export_path) - target_path = '%s/%s' % (storage_path, clone_name) - self.zapi_client.clone_file('%s/%s' % (storage_path, volume_name), - target_path) - - def _update_volume_stats(self): - """Retrieve stats info from vserver.""" - - self._ensure_shares_mounted() - - LOG.debug('Updating volume stats') - data = {} - netapp_backend = 'NetApp_NFS_7mode_direct' - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or netapp_backend - data['vendor_name'] = 'NetApp' - data['driver_version'] = self.VERSION - data['storage_protocol'] = 'nfs' - data['pools'] = self._get_pool_stats() - - self._spawn_clean_cache_job() - na_utils.provide_ems(self, self._client, netapp_backend, - self._app_version, server_type="7mode") - self._stats = data - - def _get_pool_stats(self): - """Retrieve pool (i.e. NFS share) stats info from SSC volumes.""" - - pools = [] - - for nfs_share in self._mounted_shares: - - capacity = self._get_extended_capacity_info(nfs_share) - - pool = dict() - pool['pool_name'] = nfs_share - pool['QoS_support'] = False - pool['reserved_percentage'] = 0 - - # Report pool as reserved when over the configured used_ratio - if capacity['used_ratio'] > self.configuration.nfs_used_ratio: - pool['reserved_percentage'] = 100 - - # Report pool as reserved when over the subscribed ratio - if capacity['subscribed_ratio'] >=\ - self.configuration.nfs_oversub_ratio: - pool['reserved_percentage'] = 100 - - # convert sizes to GB - total = float(capacity['apparent_size']) / units.Gi - pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') - - free = float(capacity['apparent_available']) / units.Gi - pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') - - pools.append(pool) - - return pools - - def _shortlist_del_eligible_files(self, share, old_files): - """Prepares list of eligible files to be deleted from cache.""" - file_list = [] - exp_volume = self.zapi_client.get_actual_path_for_export(share) - for file in old_files: - path = '/vol/%s/%s' % (exp_volume, file) - u_bytes = self.zapi_client.get_file_usage(path) - file_list.append((file, u_bytes)) - LOG.debug('Shortlisted del elg files %s', file_list) - return file_list - - def _is_filer_ip(self, ip): - """Checks whether ip is on the same filer.""" - try: - ifconfig = self.zapi_client.get_ifconfig() - if_info = ifconfig.get_child_by_name('interface-config-info') - if if_info: - ifs = if_info.get_children() - for intf in ifs: - v4_addr = intf.get_child_by_name('v4-primary-address') - if v4_addr: - ip_info = v4_addr.get_child_by_name('ip-address-info') - if ip_info: - address = ip_info.get_child_content('address') - if ip == address: - return True - else: - continue - except Exception: - return False - return False - - def _share_match_for_ip(self, ip, shares): - """Returns the share that is served by ip. - - Multiple shares can have same dir path but - can be served using different ips. It finds the - share which is served by ip on same nfs server. - """ - if self._is_filer_ip(ip) and shares: - for share in shares: - ip_sh = share.split(':')[0] - if self._is_filer_ip(ip_sh): - LOG.debug('Share match found for ip %s', ip) - return share - LOG.debug('No share match found for ip %s', ip) - return None - - def _is_share_vol_compatible(self, volume, share): - """Checks if share is compatible with volume to host it.""" - return self._is_share_eligible(share, volume['size']) diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index ad217c35df0..9e979d2b656 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -1,6 +1,6 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# All Rights Reserved. +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Bob Callaway. 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 diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index 364eddcce84..971e16a63e8 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -1,6 +1,6 @@ -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack Foundation -# All Rights Reserved. +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. 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 @@ -20,28 +20,20 @@ This module contains common utilities to be used by one or more NetApp drivers to achieve the desired functionality. """ -import base64 -import binascii -import copy + import decimal import platform import socket -import uuid from oslo.concurrency import processutils as putils -from oslo.utils import timeutils import six from cinder import context from cinder import exception -from cinder.i18n import _, _LW +from cinder.i18n import _, _LW, _LI from cinder.openstack.common import log as logging from cinder import utils from cinder import version -from cinder.volume.drivers.netapp.api import NaApiError -from cinder.volume.drivers.netapp.api import NaElement -from cinder.volume.drivers.netapp.api import NaErrors -from cinder.volume.drivers.netapp.api import NaServer from cinder.volume import volume_types @@ -56,95 +48,6 @@ DEPRECATED_SSC_SPECS = {'netapp_unmirrored': 'netapp_mirrored', 'netapp_thick_provisioned': 'netapp_thin_provisioned'} -def provide_ems(requester, server, netapp_backend, app_version, - server_type="cluster"): - """Provide ems with volume stats for the requester. - - :param server_type: cluster or 7mode. - """ - - def _create_ems(netapp_backend, app_version, server_type): - """Create ems api request.""" - ems_log = NaElement('ems-autosupport-log') - host = socket.getfqdn() or 'Cinder_node' - if server_type == "cluster": - dest = "cluster node" - else: - dest = "7 mode controller" - ems_log.add_new_child('computer-name', host) - ems_log.add_new_child('event-id', '0') - ems_log.add_new_child('event-source', - 'Cinder driver %s' % netapp_backend) - ems_log.add_new_child('app-version', app_version) - ems_log.add_new_child('category', 'provisioning') - ems_log.add_new_child('event-description', - 'OpenStack Cinder connected to %s' % dest) - ems_log.add_new_child('log-level', '6') - ems_log.add_new_child('auto-support', 'false') - return ems_log - - def _create_vs_get(): - """Create vs_get api request.""" - vs_get = NaElement('vserver-get-iter') - vs_get.add_new_child('max-records', '1') - query = NaElement('query') - query.add_node_with_children('vserver-info', - **{'vserver-type': 'node'}) - vs_get.add_child_elem(query) - desired = NaElement('desired-attributes') - desired.add_node_with_children( - 'vserver-info', **{'vserver-name': '', 'vserver-type': ''}) - vs_get.add_child_elem(desired) - return vs_get - - def _get_cluster_node(na_server): - """Get the cluster node for ems.""" - na_server.set_vserver(None) - vs_get = _create_vs_get() - res = na_server.invoke_successfully(vs_get) - if (res.get_child_content('num-records') and - int(res.get_child_content('num-records')) > 0): - attr_list = res.get_child_by_name('attributes-list') - vs_info = attr_list.get_child_by_name('vserver-info') - vs_name = vs_info.get_child_content('vserver-name') - return vs_name - return None - - do_ems = True - if hasattr(requester, 'last_ems'): - sec_limit = 3559 - if not (timeutils.is_older_than(requester.last_ems, sec_limit)): - do_ems = False - if do_ems: - na_server = copy.copy(server) - na_server.set_timeout(25) - ems = _create_ems(netapp_backend, app_version, server_type) - try: - if server_type == "cluster": - api_version = na_server.get_api_version() - if api_version: - major, minor = api_version - else: - raise NaApiError(code='Not found', - message='No api version found') - if major == 1 and minor > 15: - node = getattr(requester, 'vserver', None) - else: - node = _get_cluster_node(na_server) - if node is None: - raise NaApiError(code='Not found', - message='No vserver found') - na_server.set_vserver(node) - else: - na_server.set_vfiler(None) - na_server.invoke_successfully(ems, True) - LOG.debug("ems executed successfully.") - except NaApiError as e: - LOG.warning(_LW("Failed to invoke ems. Message : %s") % e) - finally: - requester.last_ems = timeutils.utcnow() - - def validate_instantiation(**kwargs): """Checks if a driver is instantiated other than by the unified driver. @@ -157,84 +60,12 @@ def validate_instantiation(**kwargs): "Please use NetAppDriver to achieve the functionality.")) -def invoke_api(na_server, api_name, api_family='cm', query=None, - des_result=None, additional_elems=None, - is_iter=False, records=0, tag=None, - timeout=0, tunnel=None): - """Invokes any given api call to a NetApp server. - - :param na_server: na_server instance - :param api_name: api name string - :param api_family: cm or 7m - :param query: api query as dict - :param des_result: desired result as dict - :param additional_elems: dict other than query and des_result - :param is_iter: is iterator api - :param records: limit for records, 0 for infinite - :param timeout: timeout seconds - :param tunnel: tunnel entity, vserver or vfiler name - """ - record_step = 50 - if not (na_server or isinstance(na_server, NaServer)): - msg = _("Requires an NaServer instance.") - raise exception.InvalidInput(reason=msg) - server = copy.copy(na_server) - if api_family == 'cm': - server.set_vserver(tunnel) - else: - server.set_vfiler(tunnel) - if timeout > 0: - server.set_timeout(timeout) - iter_records = 0 - cond = True - while cond: - na_element = create_api_request( - api_name, query, des_result, additional_elems, - is_iter, record_step, tag) - result = server.invoke_successfully(na_element, True) - if is_iter: - if records > 0: - iter_records = iter_records + record_step - if iter_records >= records: - cond = False - tag_el = result.get_child_by_name('next-tag') - tag = tag_el.get_content() if tag_el else None - if not tag: - cond = False - else: - cond = False - yield result - - -def create_api_request(api_name, query=None, des_result=None, - additional_elems=None, is_iter=False, - record_step=50, tag=None): - """Creates a NetApp api request. - - :param api_name: api name string - :param query: api query as dict - :param des_result: desired result as dict - :param additional_elems: dict other than query and des_result - :param is_iter: is iterator api - :param record_step: records at a time for iter api - :param tag: next tag for iter api - """ - api_el = NaElement(api_name) - if query: - query_el = NaElement('query') - query_el.translate_struct(query) - api_el.add_child_elem(query_el) - if des_result: - res_el = NaElement('desired-attributes') - res_el.translate_struct(des_result) - api_el.add_child_elem(res_el) - if additional_elems: - api_el.translate_struct(additional_elems) - if is_iter: - api_el.add_new_child('max-records', six.text_type(record_step)) - if tag: - api_el.add_new_child('tag', tag, True) - return api_el +def check_flags(required_flags, configuration): + """Ensure that the flags we care about are set.""" + for flag in required_flags: + if not getattr(configuration, flag, None): + msg = _('Configuration value %s is not set.') % flag + raise exception.InvalidInput(reason=msg) def to_bool(val): @@ -282,66 +113,6 @@ def get_volume_extra_specs(volume): return specs -def check_apis_on_cluster(na_server, api_list=None): - """Checks api availability and permissions on cluster. - - Checks api availability and permissions for executing user. - Returns a list of failed apis. - """ - api_list = api_list or [] - failed_apis = [] - if api_list: - api_version = na_server.get_api_version() - if api_version: - major, minor = api_version - if major == 1 and minor < 20: - for api_name in api_list: - na_el = NaElement(api_name) - try: - na_server.invoke_successfully(na_el) - except Exception as e: - if isinstance(e, NaApiError): - if (e.code == NaErrors['API_NOT_FOUND'].code or - e.code == - NaErrors['INSUFFICIENT_PRIVS'].code): - failed_apis.append(api_name) - elif major == 1 and minor >= 20: - failed_apis = copy.copy(api_list) - result = invoke_api( - na_server, - api_name='system-user-capability-get-iter', - api_family='cm', - additional_elems=None, - is_iter=True) - for res in result: - attr_list = res.get_child_by_name('attributes-list') - if attr_list: - capabilities = attr_list.get_children() - for capability in capabilities: - op_list = capability.get_child_by_name( - 'operation-list') - if op_list: - ops = op_list.get_children() - for op in ops: - apis = op.get_child_content('api-name') - if apis: - api_list = apis.split(',') - for api_name in api_list: - if (api_name and - api_name.strip() - in failed_apis): - failed_apis.remove(api_name) - else: - continue - else: - msg = _("Unsupported Clustered Data ONTAP version.") - raise exception.VolumeBackendAPIException(data=msg) - else: - msg = _("Api version could not be determined.") - raise exception.VolumeBackendAPIException(data=msg) - return failed_apis - - def resolve_hostname(hostname): """Resolves host name to IP address.""" res = socket.getaddrinfo(hostname, None)[0] @@ -349,30 +120,6 @@ def resolve_hostname(hostname): return sockaddr[0] -def encode_hex_to_base32(hex_string): - """Encodes hex to base32 bit as per RFC4648.""" - bin_form = binascii.unhexlify(hex_string) - return base64.b32encode(bin_form) - - -def decode_base32_to_hex(base32_string): - """Decodes base32 string to hex string.""" - bin_form = base64.b32decode(base32_string) - return binascii.hexlify(bin_form) - - -def convert_uuid_to_es_fmt(uuid_str): - """Converts uuid to e-series compatible name format.""" - uuid_base32 = encode_hex_to_base32(uuid.UUID(six.text_type(uuid_str)).hex) - return uuid_base32.strip('=') - - -def convert_es_fmt_to_uuid(es_label): - """Converts e-series name format to uuid.""" - es_label_b32 = es_label.ljust(32, '=') - return uuid.UUID(binascii.hexlify(base64.b32decode(es_label_b32))) - - def round_down(value, precision): return float(decimal.Decimal(six.text_type(value)).quantize( decimal.Decimal(precision), rounding=decimal.ROUND_DOWN)) @@ -454,7 +201,7 @@ class OpenStackInfo(object): "'%{version}\t%{release}\t%{vendor}'", self.PACKAGE_NAME) if not out: - LOG.info(_('No rpm info found for %(pkg)s package.') % { + LOG.info(_LI('No rpm info found for %(pkg)s package.') % { 'pkg': self.PACKAGE_NAME}) return False parts = out.split() @@ -463,8 +210,7 @@ class OpenStackInfo(object): self._vendor = ' '.join(parts[2::]) return True except Exception as e: - LOG.info(_('Could not run rpm command: %(msg)s.') % { - 'msg': e}) + LOG.info(_LI('Could not run rpm command: %(msg)s.') % {'msg': e}) return False # ubuntu, mirantis on ubuntu @@ -475,8 +221,8 @@ class OpenStackInfo(object): out, err = putils.execute("dpkg-query", "-W", "-f='${Version}'", self.PACKAGE_NAME) if not out: - LOG.info(_('No dpkg-query info found for %(pkg)s package.') % { - 'pkg': self.PACKAGE_NAME}) + LOG.info(_LI('No dpkg-query info found for %(pkg)s package.') + % {'pkg': self.PACKAGE_NAME}) return False # debian format: [epoch:]upstream_version[-debian_revision] deb_version = out @@ -493,7 +239,7 @@ class OpenStackInfo(object): self._vendor = _vendor return True except Exception as e: - LOG.info(_('Could not run dpkg-query command: %(msg)s.') % { + LOG.info(_LI('Could not run dpkg-query command: %(msg)s.') % { 'msg': e}) return False