]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Fix use of wrong storage pools for NetApp Drivers
authorGoutham Pacha Ravi <gouthamr@netapp.com>
Fri, 26 Jun 2015 16:32:06 +0000 (12:32 -0400)
committerGoutham Pacha Ravi <gouthamr@netapp.com>
Tue, 22 Sep 2015 13:58:20 +0000 (13:58 +0000)
When using NetApp drivers, all available flexvols on
a datastore are being reported to the Cinder Scheduler
as available storage pools. This allows Cinder volumes
to be created on flexvols not designated for OpenStack.

This commit fixes the problem by delimiting which pools are
eligible for provisioning via a configuration parameter
'netapp_pool_name_search_pattern'. This allows
the Cinder drivers to operate against a restricted
set of storage containers.

DocImpact
Co-Authored-By: Rushil Chugh <rushil@netapp.com>
Co-Authored-By: Michael Price <michael.price@netapp.com>
Closes-Bug: #1493399
Change-Id: I7b6f2205470ecbbcb165889db19e1168117dd0b2

cinder/tests/unit/test_netapp.py
cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py
cinder/volume/drivers/netapp/dataontap/block_7mode.py
cinder/volume/drivers/netapp/dataontap/block_cmode.py
cinder/volume/drivers/netapp/eseries/library.py
cinder/volume/drivers/netapp/options.py
cinder/volume/drivers/netapp/utils.py

index dd7198a3701c0e2754799d27f9cc15f6d7c7a17b..dbc4bb1e962be911eb44e22a28f7b7182ba87704 100644 (file)
@@ -1,4 +1,5 @@
 # Copyright (c) 2012 NetApp, Inc.
+# Copyright (c) 2015 Goutham Pacha Ravi.  All rights reserved.
 # All rights reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -24,8 +25,11 @@ from cinder import exception
 from cinder import test
 from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
     fake_api as netapp_api)
+from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes
 from cinder.volume import configuration as conf
 from cinder.volume.drivers.netapp import common
+from cinder.volume.drivers.netapp.dataontap import block_7mode
+from cinder.volume.drivers.netapp.dataontap import block_cmode
 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
@@ -583,6 +587,9 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase):
         self.driver.library.zapi_client = mock.MagicMock()
         self.driver.library.zapi_client.get_ontapi_version.return_value = \
             (1, 20)
+        self.mock_object(block_cmode.NetAppBlockStorageCmodeLibrary,
+                         '_get_filtered_pools',
+                         mock.Mock(return_value=fakes.FAKE_CMODE_POOLS))
         self.driver.check_for_setup_error()
 
     def test_do_setup_all_default(self):
@@ -1279,6 +1286,9 @@ class NetAppDirect7modeISCSIDriverTestCase_NV(test.TestCase):
         self.driver.library.zapi_client = mock.MagicMock()
         self.driver.library.zapi_client.get_ontapi_version.\
             return_value = (1, 20)
+        self.mock_object(block_7mode.NetAppBlockStorage7modeLibrary,
+                         '_get_filtered_pools',
+                         mock.Mock(return_value=fakes.FAKE_7MODE_POOLS))
         self.driver.check_for_setup_error()
 
     def test_check_for_setup_error_version(self):
index b7f61ff97a8f6a6bc0661bf8a72c57b3a9a1ac42..f097d4943f1db2bdc00b7606a7a950438e6c605a 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from lxml import etree
+
 from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
     fake_api as netapp_api)
+from cinder.volume.drivers.netapp.dataontap import ssc_cmode
+
 
 VOLUME_ID = 'f10d1a84-9b7b-427e-8fec-63c48b509a56'
 LUN_ID = 'ee6b4cc7-477b-4016-aa0c-7127b4e3af86'
@@ -212,6 +216,80 @@ SNAPSHOT = {
 
 VOLUME_REF = {'name': 'fake_vref_name', 'size': 42}
 
+FAKE_CMODE_POOLS = [
+    {
+        'QoS_support': True,
+        'free_capacity_gb': 3.72,
+        'netapp_compression': u'true',
+        'netapp_dedup': u'true',
+        'netapp_disk_type': 'SSD',
+        'netapp_mirrored': u'true',
+        'netapp_nocompression': u'false',
+        'netapp_nodedup': u'false',
+        'netapp_raid_type': 'raiddp',
+        'netapp_thick_provisioned': u'false',
+        'netapp_thin_provisioned': u'true',
+        'netapp_unmirrored': u'false',
+        'pool_name': 'open123',
+        'reserved_percentage': 0,
+        'total_capacity_gb': 4.65,
+        'thin_provisioned_support': True,
+        'thick_provisioned_support': False,
+        'provisioned_capacity_gb': 0.93,
+        'max_over_subscription_ratio': 20.0,
+    }
+]
+
+FAKE_CMODE_VOLUME = {
+    'all': [ssc_cmode.NetAppVolume(name='open123', vserver='vs'),
+            ssc_cmode.NetAppVolume(name='mixed', vserver='vs'),
+            ssc_cmode.NetAppVolume(name='open321', vserver='vs')],
+}
+
+FAKE_7MODE_VOLUME = {
+    'all': [
+        netapp_api.NaElement(
+            etree.XML("""<volume-info xmlns="http://www.netapp.com/filer/admin">
+            <name>open123</name>
+            </volume-info>""")),
+        netapp_api.NaElement(
+            etree.XML("""<volume-info xmlns="http://www.netapp.com/filer/admin">
+            <name>mixed3</name>
+            </volume-info>""")),
+        netapp_api.NaElement(
+            etree.XML("""<volume-info xmlns="http://www.netapp.com/filer/admin">
+            <name>open1234</name>
+            </volume-info>"""))
+    ],
+}
+
+
+FAKE_CMODE_VOL1 = ssc_cmode.NetAppVolume(name='open123', vserver='openstack')
+FAKE_CMODE_VOL1.state['vserver_root'] = False
+FAKE_CMODE_VOL1.state['status'] = 'online'
+FAKE_CMODE_VOL1.state['junction_active'] = True
+FAKE_CMODE_VOL1.space['size_avl_bytes'] = '4000000000'
+FAKE_CMODE_VOL1.space['size_total_bytes'] = '5000000000'
+FAKE_CMODE_VOL1.space['space-guarantee-enabled'] = False
+FAKE_CMODE_VOL1.space['space-guarantee'] = 'file'
+FAKE_CMODE_VOL1.space['thin_provisioned'] = True
+FAKE_CMODE_VOL1.mirror['mirrored'] = True
+FAKE_CMODE_VOL1.qos['qos_policy_group'] = None
+FAKE_CMODE_VOL1.aggr['name'] = 'aggr1'
+FAKE_CMODE_VOL1.aggr['junction'] = '/vola'
+FAKE_CMODE_VOL1.sis['dedup'] = True
+FAKE_CMODE_VOL1.sis['compression'] = True
+FAKE_CMODE_VOL1.aggr['raid_type'] = 'raiddp'
+FAKE_CMODE_VOL1.aggr['ha_policy'] = 'cfo'
+FAKE_CMODE_VOL1.aggr['disk_type'] = 'SSD'
+ssc_map = {
+    'mirrored': [FAKE_CMODE_VOL1],
+    'dedup': [FAKE_CMODE_VOL1],
+    'compression': [FAKE_CMODE_VOL1],
+    'thin': [FAKE_CMODE_VOL1],
+    'all': [FAKE_CMODE_VOL1],
+}
+
 FILE_LIST = ['file1', 'file2', 'file3']
 
 FAKE_LUN = netapp_api.NaElement.create_node_with_children(
@@ -239,6 +317,31 @@ FAKE_LUN = netapp_api.NaElement.create_node_with_children(
        'volume': 'fakeLUN',
        'vserver': 'fake_vserver'})
 
+FAKE_7MODE_VOL1 = [netapp_api.NaElement(
+    etree.XML("""<volume-info xmlns="http://www.netapp.com/filer/admin">
+    <name>open123</name>
+    <state>online</state>
+    <size-total>0</size-total>
+    <size-used>0</size-used>
+    <size-available>0</size-available>
+    <is-inconsistent>false</is-inconsistent>
+    <is-invalid>false</is-invalid>
+    </volume-info>"""))]
+
+FAKE_7MODE_POOLS = [
+    {
+        'pool_name': 'open123',
+        'QoS_support': False,
+        'reserved_percentage': 0,
+        'total_capacity_gb': 0.0,
+        'free_capacity_gb': 0.0,
+        'max_over_subscription_ratio': 20.0,
+        'thin_provisioned_support': False,
+        'thick_provisioned_support': True,
+        'provisioned_capacity_gb': 0.0,
+    }
+]
+
 
 class test_volume(object):
     pass
index c181ed60b191bf40204896b2bf47f435c82aba4c..a217506c3a10cab73497fcb3a402270565e97eec 100644 (file)
@@ -1,6 +1,7 @@
 # Copyright (c) 2014 Alex Meade.  All rights reserved.
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
 # Copyright (c) 2015 Tom Barron.  All rights reserved.
+# Copyright (c) 2015 Goutham Pacha Ravi. 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
@@ -44,7 +45,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
         super(NetAppBlockStorage7modeLibraryTestCase, self).setUp()
 
         # Inject fake netapp_lib module classes.
-        netapp_api.mock_netapp_lib([block_7mode])
+        netapp_api.mock_netapp_lib([block_7mode, client_base])
 
         kwargs = {'configuration': self.get_config_7mode()}
         self.library = block_7mode.NetAppBlockStorage7modeLibrary(
@@ -53,6 +54,8 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
         self.library.zapi_client = mock.Mock()
         self.zapi_client = self.library.zapi_client
         self.library.vfiler = mock.Mock()
+        # Deprecated option
+        self.library.configuration.netapp_volume_list = None
 
     def tearDown(self):
         super(NetAppBlockStorage7modeLibraryTestCase, self).tearDown()
@@ -106,11 +109,21 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
         block_base.NetAppBlockStorageLibrary, 'check_for_setup_error')
     def test_check_for_setup_error(self, super_check_for_setup_error):
         self.zapi_client.get_ontapi_version.return_value = (1, 9)
+        self.mock_object(self.library, '_refresh_volume_info')
+        self.library.volume_list = ['open1', 'open2']
 
         self.library.check_for_setup_error()
 
         super_check_for_setup_error.assert_called_once_with()
 
+    def test_check_for_setup_error_no_filtered_pools(self):
+        self.zapi_client.get_ontapi_version.return_value = (1, 9)
+        self.mock_object(self.library, '_refresh_volume_info')
+        self.library.volume_list = []
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.check_for_setup_error)
+
     def test_check_for_setup_error_too_old(self):
         self.zapi_client.get_ontapi_version.return_value = (1, 8)
         self.assertRaises(exception.VolumeBackendAPIException,
@@ -422,7 +435,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
 
     def test_manage_existing_lun_same_name(self):
         mock_lun = block_base.NetAppLun('handle', 'name', '1',
-                                        {'Path': '/vol/vol1/name'})
+                                        {'Path': '/vol/FAKE_CMODE_VOL1/name'})
         self.library._get_existing_vol_with_manage_ref = mock.Mock(
             return_value=mock_lun)
         self.mock_object(na_utils, 'get_volume_extra_specs')
@@ -441,7 +454,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
 
     def test_manage_existing_lun_new_path(self):
         mock_lun = block_base.NetAppLun(
-            'handle', 'name', '1', {'Path': '/vol/vol1/name'})
+            'handle', 'name', '1', {'Path': '/vol/FAKE_CMODE_VOL1/name'})
         self.library._get_existing_vol_with_manage_ref = mock.Mock(
             return_value=mock_lun)
         self.mock_object(na_utils, 'get_volume_extra_specs')
@@ -457,7 +470,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
         self.assertEqual(1, self.library._check_volume_type_for_lun.call_count)
         self.assertEqual(1, self.library._add_lun_to_table.call_count)
         self.zapi_client.move_lun.assert_called_once_with(
-            '/vol/vol1/name', '/vol/vol1/volume')
+            '/vol/FAKE_CMODE_VOL1/name', '/vol/FAKE_CMODE_VOL1/volume')
 
     def test_get_pool_stats_no_volumes(self):
 
@@ -499,3 +512,87 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
         }]
 
         self.assertEqual(expected, result)
+
+    def test_get_filtered_pools_invalid_conf(self):
+        """Verify an exception is raised if the regex pattern is invalid."""
+        self.library.configuration.netapp_pool_name_search_pattern = '(.+'
+
+        self.assertRaises(exception.InvalidConfigurationValue,
+                          self.library._get_filtered_pools)
+
+    @ddt.data('.*?3$|mix.+', '(.+?[0-9]+) ', '^.+3$', '^[a-z].*?[^4]$')
+    def test_get_filtered_pools_match_select_pools(self, patterns):
+        self.library.vols = fake.FAKE_7MODE_VOLUME['all']
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertEqual(
+            fake.FAKE_7MODE_VOLUME['all'][0].get_child_content('name'),
+            filtered_pools[0]
+        )
+        self.assertEqual(
+            fake.FAKE_7MODE_VOLUME['all'][1].get_child_content('name'),
+            filtered_pools[1]
+        )
+
+    @ddt.data('', 'mix.+|open.+', '.+', 'open123, mixed3, open1234', '.+')
+    def test_get_filtered_pools_match_all_pools(self, patterns):
+        self.library.vols = fake.FAKE_7MODE_VOLUME['all']
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertEqual(
+            fake.FAKE_7MODE_VOLUME['all'][0].get_child_content('name'),
+            filtered_pools[0]
+        )
+        self.assertEqual(
+            fake.FAKE_7MODE_VOLUME['all'][1].get_child_content('name'),
+            filtered_pools[1]
+        )
+        self.assertEqual(
+            fake.FAKE_7MODE_VOLUME['all'][2].get_child_content('name'),
+            filtered_pools[2]
+        )
+
+    @ddt.data('abc|stackopen|openstack|abc.*', 'abc',
+              'stackopen, openstack, open', '^$')
+    def test_get_filtered_pools_non_matching_patterns(self, patterns):
+
+        self.library.vols = fake.FAKE_7MODE_VOLUME['all']
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertListEqual([], filtered_pools)
+
+    def test_get_pool_stats_no_ssc_vols(self):
+
+        self.library.vols = {}
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual([], pools)
+
+    def test_get_pool_stats_with_filtered_pools(self):
+
+        self.library.vols = fake.FAKE_7MODE_VOL1
+        self.library.volume_list = [
+            fake.FAKE_7MODE_VOL1[0].get_child_content('name')
+        ]
+        self.library.root_volume_name = ''
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual(fake.FAKE_7MODE_POOLS, pools)
+
+    def test_get_pool_stats_no_filtered_pools(self):
+
+        self.library.vols = fake.FAKE_7MODE_VOL1
+        self.library.volume_list = ['open1', 'open2']
+        self.library.root_volume_name = ''
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual([], pools)
index 1008b5f66e627b48d1fbc598f525165a7ffa82ea..2b377bb3631427d40b041ba7e513560697ce6639 100644 (file)
@@ -2,6 +2,7 @@
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
 # Copyright (c) 2014 Andrew Kerr.  All rights reserved.
 # Copyright (c) 2015 Tom Barron.  All rights reserved.
+# Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved.
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -87,10 +88,10 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
 
     @mock.patch.object(block_base.NetAppBlockStorageLibrary,
                        '_get_lun_attr',
-                       mock.Mock(return_value={'Volume': 'vol1'}))
+                       mock.Mock(return_value={'Volume': 'FAKE_CMODE_VOL1'}))
     def test_get_pool(self):
         pool = self.library.get_pool({'name': 'volume-fake-uuid'})
-        self.assertEqual('vol1', pool)
+        self.assertEqual('FAKE_CMODE_VOL1', pool)
 
     @mock.patch.object(block_base.NetAppBlockStorageLibrary,
                        '_get_lun_attr',
index bf8805a19a3ab9adf0a8ce7343fbdcf0e4cc5a8a..ed26b7535bd5374dd27aca3cdeec059c58041faf 100644 (file)
@@ -91,6 +91,9 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
             ssc_cmode, 'check_ssc_api_permissions')
         mock_start_periodic_tasks = self.mock_object(
             self.library, '_start_periodic_tasks')
+        self.mock_object(ssc_cmode, 'refresh_cluster_ssc')
+        self.mock_object(self.library, '_get_filtered_pools',
+                         mock.Mock(return_value=fake.FAKE_CMODE_POOLS))
 
         self.library.check_for_setup_error()
 
@@ -99,6 +102,18 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
             self.library.zapi_client)
         self.assertEqual(1, mock_start_periodic_tasks.call_count)
 
+    def test_check_for_setup_error_no_filtered_pools(self):
+        self.mock_object(block_base.NetAppBlockStorageLibrary,
+                         'check_for_setup_error')
+        self.mock_object(ssc_cmode, 'check_ssc_api_permissions')
+        self.mock_object(self.library, '_start_periodic_tasks')
+        self.mock_object(ssc_cmode, 'refresh_cluster_ssc')
+        self.mock_object(self.library, '_get_filtered_pools',
+                         mock.Mock(return_value=[]))
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.check_for_setup_error)
+
     def test_find_mapped_lun_igroup(self):
         igroups = [fake.IGROUP1]
         self.zapi_client.get_igroup_by_initiators.return_value = igroups
@@ -472,7 +487,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
 
     def test_manage_existing_lun_same_name(self):
         mock_lun = block_base.NetAppLun('handle', 'name', '1',
-                                        {'Path': '/vol/vol1/name'})
+                                        {'Path': '/vol/FAKE_CMODE_VOL1/name'})
         self.library._get_existing_vol_with_manage_ref = mock.Mock(
             return_value=mock_lun)
         self.mock_object(na_utils, 'get_volume_extra_specs')
@@ -497,7 +512,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
 
     def test_manage_existing_lun_new_path(self):
         mock_lun = block_base.NetAppLun(
-            'handle', 'name', '1', {'Path': '/vol/vol1/name'})
+            'handle', 'name', '1', {'Path': '/vol/FAKE_CMODE_VOL1/name'})
         self.library._get_existing_vol_with_manage_ref = mock.Mock(
             return_value=mock_lun)
         self.mock_object(na_utils, 'get_volume_extra_specs')
@@ -513,7 +528,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
         self.assertEqual(1, self.library._check_volume_type_for_lun.call_count)
         self.assertEqual(1, self.library._add_lun_to_table.call_count)
         self.zapi_client.move_lun.assert_called_once_with(
-            '/vol/vol1/name', '/vol/vol1/volume')
+            '/vol/FAKE_CMODE_VOL1/name', '/vol/FAKE_CMODE_VOL1/volume')
 
     def test_start_periodic_tasks(self):
 
@@ -532,3 +547,80 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
         mock_loopingcall.assert_has_calls([
             mock.call(mock_remove_unused_qos_policy_groups)])
         self.assertTrue(harvest_qos_periodic_task.start.called)
+
+    @ddt.data('open+|demix+', 'open.+', '.+\d', '^((?!mix+).)*$',
+              'open123, open321')
+    def test_get_filtered_pools_match_selected_pools(self, patterns):
+
+        self.library.ssc_vols = fake.FAKE_CMODE_VOLUME
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertEqual(fake.FAKE_CMODE_VOLUME['all'][0].id['name'],
+                         filtered_pools[0].id['name'])
+        self.assertEqual(fake.FAKE_CMODE_VOLUME['all'][2].id['name'],
+                         filtered_pools[1].id['name'])
+
+    @ddt.data('', 'mix.+|open.+', '.+', 'open123, mixed, open321',
+              '.*?')
+    def test_get_filtered_pools_match_all_pools(self, patterns):
+
+        self.library.ssc_vols = fake.FAKE_CMODE_VOLUME
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertEqual(fake.FAKE_CMODE_VOLUME['all'][0].id['name'],
+                         filtered_pools[0].id['name'])
+        self.assertEqual(fake.FAKE_CMODE_VOLUME['all'][1].id['name'],
+                         filtered_pools[1].id['name'])
+        self.assertEqual(fake.FAKE_CMODE_VOLUME['all'][2].id['name'],
+                         filtered_pools[2].id['name'])
+
+    def test_get_filtered_pools_invalid_conf(self):
+        """Verify an exception is raised if the regex pattern is invalid"""
+        self.library.configuration.netapp_pool_name_search_pattern = '(.+'
+
+        self.assertRaises(exception.InvalidConfigurationValue,
+                          self.library._get_filtered_pools)
+
+    @ddt.data('abc|stackopen|openstack|abc*', 'abc', 'stackopen', 'openstack',
+              'abc*', '^$')
+    def test_get_filtered_pools_non_matching_patterns(self, patterns):
+
+        self.library.ssc_vols = fake.FAKE_CMODE_VOLUME
+        self.library.configuration.netapp_pool_name_search_pattern = patterns
+
+        filtered_pools = self.library._get_filtered_pools()
+
+        self.assertListEqual([], filtered_pools)
+
+    @ddt.data({}, None)
+    def test_get_pool_stats_no_ssc_vols(self, vols):
+
+        self.library.ssc_vols = vols
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual([], pools)
+
+    def test_get_pool_stats_with_filtered_pools(self):
+
+        self.library.ssc_vols = fake.ssc_map
+        self.mock_object(self.library, '_get_filtered_pools',
+                         mock.Mock(return_value=[fake.FAKE_CMODE_VOL1]))
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual(fake.FAKE_CMODE_POOLS, pools)
+
+    def test_get_pool_stats_no_filtered_pools(self):
+
+        self.library.ssc_vols = fake.ssc_map
+        self.mock_object(self.library, '_get_filtered_pools',
+                         mock.Mock(return_value=[]))
+
+        pools = self.library._get_pool_stats()
+
+        self.assertListEqual([], pools)
index eb0f9a48392235f9bb135ae53f90d0670ec688ed..de69391b6d1a1c5e1a18ef6c0422497c8a75ccb9 100644 (file)
@@ -60,6 +60,8 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
                   eseries_fake.create_configuration_eseries()}
 
         self.library = library.NetAppESeriesLibrary('FAKE', **kwargs)
+        # Deprecated Option
+        self.library.configuration.netapp_storage_pools = None
         self.library._client = eseries_fake.FakeEseriesClient()
         self.library.check_for_setup_error()
 
@@ -73,19 +75,54 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
 
         self.assertTrue(mock_check_flags.called)
 
-    def test_get_storage_pools(self):
-        pool_labels = list()
-        # Retrieve the first pool's label
-        for pool in eseries_fake.STORAGE_POOLS:
-            pool_labels.append(pool['label'])
-            break
-        self.library.configuration.netapp_storage_pools = (
-            ",".join(pool_labels))
+    def test_get_storage_pools_empty_result(self):
+        """Verify an exception is raised if no pools are returned."""
+        self.library.configuration.netapp_pool_name_search_pattern = '$'
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.check_for_setup_error)
+
+    def test_get_storage_pools_invalid_conf(self):
+        """Verify an exception is raised if the regex pattern is invalid."""
+        self.library.configuration.netapp_pool_name_search_pattern = '(.*'
+
+        self.assertRaises(exception.InvalidConfigurationValue,
+                          self.library._get_storage_pools)
+
+    def test_get_storage_pools_default(self):
+        """Verify that all pools are returned if the search option is empty."""
+        filtered_pools = self.library._get_storage_pools()
+
+        self.assertEqual(eseries_fake.STORAGE_POOLS, filtered_pools)
+
+    @ddt.data(('[\d]+,a', ['1', '2', 'a', 'b'], ['1', '2', 'a']),
+              ('1   ,    3', ['1', '2', '3'], ['1', '3']),
+              ('$,3', ['1', '2', '3'], ['3']),
+              ('[a-zA-Z]+', ['1', 'a', 'B'], ['a', 'B']),
+              ('', ['1', '2'], ['1', '2'])
+              )
+    @ddt.unpack
+    def test_get_storage_pools(self, pool_filter, pool_labels,
+                               expected_pool_labels):
+        """Verify that pool filtering via the search_pattern works correctly
+
+        :param pool_filter: A regular expression to be used for filtering via
+         pool labels
+        :param pool_labels: A list of pool labels
+        :param expected_pool_labels: The labels from 'pool_labels' that
+         should be matched by 'pool_filter'
+        """
+        self.library.configuration.netapp_pool_name_search_pattern = (
+            pool_filter)
+        pools = [{'label': label} for label in pool_labels]
+
+        self.library._client.list_storage_pools = mock.Mock(
+            return_value=pools)
 
         filtered_pools = self.library._get_storage_pools()
 
         filtered_pool_labels = [pool['label'] for pool in filtered_pools]
-        self.assertListEqual(pool_labels, filtered_pool_labels)
+        self.assertEqual(expected_pool_labels, filtered_pool_labels)
 
     def test_get_volume(self):
         fake_volume = copy.deepcopy(get_fake_volume())
@@ -120,7 +157,7 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
             False, minimum_version="1.53.9000.1")
         self.library._client.SSC_VALID_VERSIONS = [(1, 53, 9000, 1),
                                                    (1, 53, 9010, 15)]
-        self.library.configuration.netapp_storage_pools = "test_vg1"
+        self.library.configuration.netapp_pool_name_search_pattern = "test_vg1"
         self.library._client.list_storage_pools = mock.Mock(return_value=pools)
         self.library._client.list_drives = mock.Mock(return_value=drives)
 
index d3d6dc0f9c6a1c7571be623146a549848ca07850..c93fdab17451d2fd87c88d223e33cf415e9e7308 100644 (file)
@@ -6,6 +6,7 @@
 # Copyright (c) 2014 Andrew Kerr.  All rights reserved.
 # Copyright (c) 2014 Jeff Applewhite.  All rights reserved.
 # Copyright (c) 2015 Tom Barron.  All rights reserved.
+# Copyright (c) 2015 Goutham Pacha Ravi. 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,6 +24,7 @@ Volume driver library for NetApp 7-mode block storage systems.
 """
 
 from oslo_log import log as logging
+from oslo_log import versionutils
 from oslo_utils import timeutils
 from oslo_utils import units
 import six
@@ -54,10 +56,7 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary):
     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.volume_list = []
 
         self.vfiler = self.configuration.netapp_vfiler
 
@@ -108,6 +107,14 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary):
         else:
             msg = _("API version could not be determined.")
             raise exception.VolumeBackendAPIException(data=msg)
+
+        self._refresh_volume_info()
+
+        if not self.volume_list:
+            msg = _('No pools are available for provisioning volumes. '
+                    'Ensure that the configuration option '
+                    'netapp_pool_name_search_pattern is set correctly.')
+            raise exception.NetAppDriverException(msg)
         super(NetAppBlockStorage7modeLibrary, self).check_for_setup_error()
 
     def _create_lun(self, volume_name, lun_name, size,
@@ -254,13 +261,12 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary):
         """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')
+
+            # omit volumes not specified in the config
             if self.volume_list and volume_name not in self.volume_list:
                 continue
 
@@ -306,6 +312,35 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary):
 
         return pools
 
+    def _get_filtered_pools(self):
+        """Return available pools filtered by a pool name search pattern."""
+
+        # Inform deprecation of legacy option.
+        if self.configuration.safe_get('netapp_volume_list'):
+            msg = _LW("The option 'netapp_volume_list' is deprecated and "
+                      "will be removed in the future releases. Please use "
+                      "the option 'netapp_pool_name_search_pattern' instead.")
+            versionutils.report_deprecated_feature(LOG, msg)
+
+        pool_regex = na_utils.get_pool_name_filter_regex(self.configuration)
+
+        filtered_pools = []
+        for vol in self.vols:
+            vol_name = vol.get_child_content('name')
+            if pool_regex.match(vol_name):
+                msg = ("Volume '%(vol_name)s' matches against regular "
+                       "expression: %(vol_pattern)s")
+                LOG.debug(msg, {'vol_name': vol_name,
+                                'vol_pattern': pool_regex.pattern})
+                filtered_pools.append(vol_name)
+            else:
+                msg = ("Volume '%(vol_name)s' does not match against regular "
+                       "expression: %(vol_pattern)s")
+                LOG.debug(msg, {'vol_name': vol_name,
+                                'vol_pattern': pool_regex.pattern})
+
+        return filtered_pools
+
     def _get_lun_block_count(self, path):
         """Gets block counts for the LUN."""
         bs = super(NetAppBlockStorage7modeLibrary,
@@ -333,6 +368,7 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary):
                     return
                 self.vol_refresh_voluntary = False
                 self.vols = self.zapi_client.get_filer_volumes()
+                self.volume_list = self._get_filtered_pools()
                 self.vol_refresh_time = timeutils.utcnow()
             except Exception as e:
                 LOG.warning(_LW("Error refreshing volume info. Message: %s"),
index 7531acaad7dee5c1b36d05569876939a108d5e08..896f42bfb3339dcc27873e27c6dc575e037b7f9b 100644 (file)
@@ -6,6 +6,7 @@
 # Copyright (c) 2014 Andrew Kerr.  All rights reserved.
 # Copyright (c) 2014 Jeff Applewhite.  All rights reserved.
 # Copyright (c) 2015 Tom Barron.  All rights reserved.
+# Copyright (c) 2015 Goutham Pacha Ravi. 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
@@ -70,12 +71,19 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary):
             port=self.configuration.netapp_server_port,
             vserver=self.vserver)
 
-        self.ssc_vols = None
+        self.ssc_vols = {}
         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)
+        ssc_cmode.refresh_cluster_ssc(self, self.zapi_client.get_connection(),
+                                      self.vserver, synchronous=True)
+        if not self._get_filtered_pools():
+            msg = _('No pools are available for provisioning volumes. '
+                    'Ensure that the configuration option '
+                    'netapp_pool_name_search_pattern is set correctly.')
+            raise exception.NetAppDriverException(msg)
         super(NetAppBlockStorageCmodeLibrary, self).check_for_setup_error()
         self._start_periodic_tasks()
 
@@ -191,10 +199,11 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary):
         """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']:
+        for vol in self._get_filtered_pools():
             pool = dict()
             pool['pool_name'] = vol.id['name']
             pool['QoS_support'] = True
@@ -244,6 +253,27 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary):
 
         return pools
 
+    def _get_filtered_pools(self):
+        """Return filtered pools given a pool name search pattern."""
+        pool_regex = na_utils.get_pool_name_filter_regex(self.configuration)
+
+        filtered_pools = []
+        for vol in self.ssc_vols.get('all', []):
+            vol_name = vol.id['name']
+            if pool_regex.match(vol_name):
+                msg = ("Volume '%(vol_name)s' matches against regular "
+                       "expression: %(vol_pattern)s")
+                LOG.debug(msg, {'vol_name': vol_name,
+                                'vol_pattern': pool_regex.pattern})
+                filtered_pools.append(vol)
+            else:
+                msg = ("Volume '%(vol_name)s' does not match against regular "
+                       "expression: %(vol_pattern)s")
+                LOG.debug(msg, {'vol_name': vol_name,
+                                'vol_pattern': pool_regex.pattern})
+
+        return filtered_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."""
index 255713d389367d027ffa006f54222546899a4705..323af2d6c810f18d006384acac9ae613fa61ca1d 100644 (file)
@@ -26,6 +26,7 @@ import uuid
 
 from oslo_config import cfg
 from oslo_log import log as logging
+from oslo_log import versionutils
 from oslo_service import loopingcall
 from oslo_utils import excutils
 from oslo_utils import units
@@ -62,8 +63,7 @@ class NetAppESeriesLibrary(object):
     AUTOSUPPORT_INTERVAL_SECONDS = 3600  # hourly
     VERSION = "1.0.0"
     REQUIRED_FLAGS = ['netapp_server_hostname', 'netapp_controller_ips',
-                      'netapp_login', 'netapp_password',
-                      'netapp_storage_pools']
+                      'netapp_login', 'netapp_password']
     SLEEP_SECS = 5
     HOST_TYPES = {'aix': 'AIX MPIO',
                   'avt': 'AVT_4M',
@@ -177,6 +177,7 @@ class NetAppESeriesLibrary(object):
     def check_for_setup_error(self):
         self._check_host_type()
         self._check_multipath()
+        self._check_pools()
         self._check_storage_system()
         self._start_periodic_tasks()
 
@@ -197,6 +198,14 @@ class NetAppESeriesLibrary(object):
                         {'backend': self._backend_name,
                          'mpflag': 'use_multipath_for_image_xfer'})
 
+    def _check_pools(self):
+        """Ensure that the pool listing contains at least one pool"""
+        if not self._get_storage_pools():
+            msg = _('No pools are available for provisioning volumes. '
+                    'Ensure that the configuration option '
+                    'netapp_pool_name_search_pattern is set correctly.')
+            raise exception.NetAppDriverException(msg)
+
     def _ensure_multi_attach_host_group_exists(self):
         try:
             host_group = self._client.get_host_group_by_name(
@@ -1115,17 +1124,34 @@ class NetAppESeriesLibrary(object):
         return ssc_stats
 
     def _get_storage_pools(self):
-        conf_enabled_pools = []
-        for value in self.configuration.netapp_storage_pools.split(','):
-            if value:
-                conf_enabled_pools.append(value.strip().lower())
+        """Retrieve storage pools that match user-configured search pattern."""
+
+        # Inform deprecation of legacy option.
+        if self.configuration.safe_get('netapp_storage_pools'):
+            msg = _LW("The option 'netapp_storage_pools' is deprecated and "
+                      "will be removed in the future releases. Please use "
+                      "the option 'netapp_pool_name_search_pattern' instead.")
+            versionutils.report_deprecated_feature(LOG, msg)
+
+        pool_regex = na_utils.get_pool_name_filter_regex(self.configuration)
 
-        filtered_pools = []
         storage_pools = self._client.list_storage_pools()
-        for storage_pool in storage_pools:
-            # Check if pool can be used
-            if (storage_pool['label'].lower() in conf_enabled_pools):
-                filtered_pools.append(storage_pool)
+
+        filtered_pools = []
+        for pool in storage_pools:
+            pool_name = pool['label']
+
+            if pool_regex.match(pool_name):
+                msg = ("Pool '%(pool_name)s' matches against regular "
+                       "expression: %(pool_pattern)s")
+                LOG.debug(msg, {'pool_name': pool_name,
+                                'pool_pattern': pool_regex.pattern})
+                filtered_pools.append(pool)
+            else:
+                msg = ("Pool '%(pool_name)s' does not match against regular "
+                       "expression: %(pool_pattern)s")
+                LOG.debug(msg, {'pool_name': pool_name,
+                                'pool_pattern': pool_regex.pattern})
 
         return filtered_pools
 
index 377b451cdeb83608e1c7016371c1005ad7ed7a10..f4df96702e3fc22f1f4a49fa74c5208e30da98eb 100644 (file)
@@ -80,14 +80,6 @@ netapp_provisioning_opts = [
                        'the volume creation request.  Note: this option '
                        'is deprecated and will be removed in favor of '
                        '"reserved_percentage" in the Mitaka release.')),
-    cfg.StrOpt('netapp_volume_list',
-               default=None,
-               help=('This option is only utilized when the storage protocol '
-                     'is configured to use iSCSI or FC. This option is used '
-                     'to restrict provisioning to the specified controller '
-                     'volumes. Specify the value of this option to be a '
-                     'comma separated list of NetApp controller volume names '
-                     'to be used for provisioning.')),
     cfg.StrOpt('netapp_lun_space_reservation',
                default='enabled',
                choices=['enabled', 'disabled'],
@@ -164,13 +156,6 @@ netapp_eseries_opts = [
                default=None,
                help=('Password for the NetApp E-Series storage array.'),
                secret=True),
-    cfg.StrOpt('netapp_storage_pools',
-               default=None,
-               help=('This option is used to restrict provisioning to the '
-                     'specified storage pools. Only dynamic disk pools are '
-                     'currently supported. Specify the value of this option to'
-                     ' be a comma separated list of disk pool names to be used'
-                     ' for provisioning.')),
     cfg.BoolOpt('netapp_enable_multiattach',
                 default=False,
                 help='This option specifies whether the driver should allow '
@@ -200,7 +185,19 @@ netapp_san_opts = [
                help=('This option defines the type of operating system for'
                      ' all initiators that can access a LUN. This information'
                      ' is used when mapping LUNs to individual hosts or'
-                     ' groups of hosts.'))]
+                     ' groups of hosts.')),
+    cfg.StrOpt('netapp_pool_name_search_pattern',
+               deprecated_opts=[cfg.DeprecatedOpt(name='netapp_volume_list'),
+                                cfg.DeprecatedOpt(name='netapp_storage_pools')
+                                ],
+               default="(.+)",
+               help=('This option is used to restrict provisioning to the '
+                     'specified pools. Specify the value of '
+                     'this option to be a regular expression which will be '
+                     'applied to the names of objects from the storage '
+                     'backend which represent pools in Cinder. This option '
+                     'is only utilized when the storage protocol is '
+                     'configured to use iSCSI or FC.')), ]
 
 CONF = cfg.CONF
 CONF.register_opts(netapp_proxy_opts)
index 81966eb09d115bd4b67fa60f4dc7616978702aee..b6a13b04d66f3f48bd9d53b32ce213f7c2398a90 100644 (file)
@@ -24,6 +24,7 @@ NetApp drivers to achieve the desired functionality.
 
 import decimal
 import platform
+import re
 import socket
 
 from oslo_concurrency import processutils as putils
@@ -247,6 +248,31 @@ def get_qos_policy_group_name_from_info(qos_policy_group_info):
     return None
 
 
+def get_pool_name_filter_regex(configuration):
+    """Build the regex for filtering pools by name
+
+    :param configuration: The volume driver configuration
+    :raise InvalidConfigurationValue: if configured regex pattern is invalid
+    :return: A compiled regex for filtering pool names
+    """
+
+    # If the configuration parameter is specified as an empty string
+    # (interpreted as matching all pools), we replace it here with
+    # (.+) to be explicit with CSV compatibility support implemented below.
+    pool_patterns = configuration.netapp_pool_name_search_pattern or '(.+)'
+
+    # Strip whitespace from start/end and then 'or' all regex patterns
+    pool_patterns = '|'.join(['^' + pool_pattern.strip('^$ \t') + '$' for
+                              pool_pattern in pool_patterns.split(',')])
+
+    try:
+        return re.compile(pool_patterns)
+    except re.error:
+        raise exception.InvalidConfigurationValue(
+            option='netapp_pool_name_search_pattern',
+            value=configuration.netapp_pool_name_search_pattern)
+
+
 def get_valid_qos_policy_group_info(volume, extra_specs=None):
     """Given a volume, return information for QOS provisioning."""
     info = dict(legacy=None, spec=None)