]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Netapp drivers support for pool-aware scheduling
authorClinton Knight <cknight@netapp.com>
Fri, 15 Aug 2014 15:12:14 +0000 (11:12 -0400)
committerClinton Knight <cknight@netapp.com>
Fri, 12 Sep 2014 17:11:24 +0000 (13:11 -0400)
Adds pools support for all NetApp drivers: eseries, 7mode (iscsi and nfs), and
cmode (iscsi and nfs). With 7mode and cmode drivers, a pool is one-to-one with a
Ontap flexvol. With eseries, a pool is one-to-one with a dynamic disk pool.

DocImpact
Implements: blueprint pool-aware-cinder-scheduler-support-in-netapp-drivers

Change-Id: Ie6f155df7bc1ae2cd5f7fa39f1b1a0ad38075988

cinder/scheduler/filters/capacity_filter.py
cinder/tests/test_netapp.py
cinder/tests/test_netapp_eseries_iscsi.py
cinder/tests/test_netapp_nfs.py
cinder/tests/volume/drivers/netapp/test_iscsi.py
cinder/tests/volume/drivers/netapp/test_utils.py [new file with mode: 0644]
cinder/volume/drivers/netapp/eseries/iscsi.py
cinder/volume/drivers/netapp/iscsi.py
cinder/volume/drivers/netapp/nfs.py
cinder/volume/drivers/netapp/ssc_utils.py
cinder/volume/drivers/netapp/utils.py

index b7dca83ab1588c2b7f564ddf8e5b57ef8bd0028d..0e64f20ce05f44c50db02a70e828dbd8eb2c70cc 100644 (file)
@@ -54,11 +54,17 @@ class CapacityFilter(filters.BaseHostFilter):
             return True
         reserved = float(host_state.reserved_percentage) / 100
         free = math.floor(free_space * (1 - reserved))
+
+        msg_args = {"host": host_state.host,
+                    "requested": volume_size,
+                    "available": free}
         if free < volume_size:
             LOG.warning(_("Insufficient free space for volume creation "
-                          "(requested / avail): "
-                          "%(requested)s/%(available)s")
-                        % {'requested': volume_size,
-                           'available': free})
+                          "on host %(host)s (requested / avail): "
+                          "%(requested)s/%(available)s") % msg_args)
+        else:
+            LOG.debug("Sufficient free space for volume creation "
+                      "on host %(host)s (requested / avail): "
+                      "%(requested)s/%(available)s" % msg_args)
 
         return free >= volume_size
index 74ff95047c2662f46d8f66fc907f3845250eeb36..29606d2a4072bc3af14b14bc056e23dc7058625e 100644 (file)
@@ -495,7 +495,7 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase):
               'os_type': 'linux', 'provider_location': 'lun1',
               'id': 'lun1', 'provider_auth': None, 'project_id': 'project',
               'display_name': None, 'display_description': 'lun1',
-              'volume_type_id': None}
+              'volume_type_id': None, 'host': 'hostname@backend#vol1'}
     snapshot = {'name': 'snapshot1', 'size': 2, 'volume_name': 'lun1',
                 'volume_size': 2, 'project_id': 'project',
                 'display_name': None, 'display_description': 'lun1',
@@ -524,7 +524,7 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase):
                 'os_type': 'linux', 'provider_location': 'lun1',
                 'id': 'lun1', 'provider_auth': None, 'project_id': 'project',
                 'display_name': None, 'display_description': 'lun1',
-                'volume_type_id': None}
+                'volume_type_id': None, 'host': 'hostname@backend#vol1'}
     vol1 = ssc_utils.NetAppVolume('lun1', 'openstack')
     vol1.state['vserver_root'] = False
     vol1.state['status'] = 'online'
@@ -623,12 +623,11 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase):
         if not properties:
             raise AssertionError('Target portal is none')
 
-    def test_fail_create_vol(self):
-        self.assertRaises(exception.VolumeBackendAPIException,
-                          self.driver.create_volume, self.vol_fail)
-
     def test_vol_stats(self):
         self.driver.get_volume_stats(refresh=True)
+        stats = self.driver._stats
+        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)
@@ -1133,6 +1132,7 @@ class NetAppDirect7modeISCSIDriverTestCase_NV(
         client = driver.client
         client.set_api_version(1, 9)
         self.driver = driver
+        self.driver.root_volume_name = 'root'
 
     def _set_config(self, configuration):
         configuration.netapp_storage_family = 'ontap_7mode'
@@ -1150,19 +1150,6 @@ class NetAppDirect7modeISCSIDriverTestCase_NV(
         self.driver.delete_volume(self.volume)
         self.driver.volume_list = []
 
-    def test_create_fail_on_select_vol(self):
-        self.driver.volume_list = ['vol2', 'vol3']
-        success = False
-        try:
-            self.driver.create_volume(self.volume)
-        except exception.VolumeBackendAPIException:
-            success = True
-            pass
-        finally:
-            self.driver.volume_list = []
-        if not success:
-            raise AssertionError('Failed creating on selected volumes')
-
     def test_check_for_setup_error_version(self):
         drv = self.driver
         delattr(drv.client, '_api_version')
@@ -1195,6 +1182,7 @@ class NetAppDirect7modeISCSIDriverTestCase_WV(
         client = driver.client
         client.set_api_version(1, 9)
         self.driver = driver
+        self.driver.root_volume_name = 'root'
 
     def _set_config(self, configuration):
         configuration.netapp_storage_family = 'ontap_7mode'
index ab03c6d78ac5a64b0d1594ade61ec0f5d5318805..f345bb4021d3cd28c32c57725e22e17fd3e8f443 100644 (file)
@@ -16,6 +16,7 @@
 Tests for NetApp e-series iscsi volume driver.
 """
 
+import copy
 import json
 import re
 
@@ -27,8 +28,12 @@ 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 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.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__)
@@ -562,7 +567,7 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase):
     """Test case for NetApp e-series iscsi driver."""
 
     volume = {'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1,
-              'volume_name': 'lun1',
+              'volume_name': 'lun1', 'host': 'hostname@backend#DDP',
               'os_type': 'linux', 'provider_location': 'lun1',
               'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef',
               'provider_auth': 'provider a b', 'project_id': 'project',
@@ -597,7 +602,10 @@ 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'])
     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()
@@ -745,3 +753,84 @@ class NetAppEseriesIscsiDriverTestCase(test.TestCase):
             self.volume_clone_large, self.snapshot)
         self.driver.delete_snapshot(self.snapshot)
         self.driver.delete_volume(self.volume)
+
+    @mock.patch.object(iscsi.Driver, '_get_volume',
+                       mock.Mock(return_value={'volumeGroupRef': 'fake_ref'}))
+    def test_get_pool(self):
+        self.driver._objects['pools'] = [{'volumeGroupRef': 'fake_ref',
+                                          'label': 'ddp1'}]
+        pool = self.driver.get_pool({'id': 'fake-uuid'})
+        self.assertEqual(pool, 'ddp1')
+
+    @mock.patch.object(iscsi.Driver, '_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.Mock(return_value={'volumeGroupRef': 'fake_ref'}))
+    def test_get_pool_no_match(self):
+        self.driver._objects['pools'] = [{'volumeGroupRef': 'fake_ref2',
+                                          'label': 'ddp2'}]
+        pool = self.driver.get_pool({'id': 'fake-uuid'})
+        self.assertEqual(pool, None)
+
+    @mock.patch.object(iscsi.Driver, '_create_volume', mock.Mock())
+    def test_create_volume(self):
+        self.driver.create_volume(self.volume)
+        self.driver._create_volume.assert_called_with(
+            'DDP', self.fake_eseries_volume_label, self.volume['size'])
+
+    def test_create_volume_no_pool_provided_by_scheduler(self):
+        volume = copy.deepcopy(self.volume)
+        volume['host'] = "host@backend"  # missing pool
+        self.assertRaises(exception.InvalidHost, self.driver.create_volume,
+                          volume)
+
+    @mock.patch.object(client.RestClient, 'list_storage_pools')
+    def test_helper_create_volume_fail(self, fake_list_pools):
+        fake_pool = {}
+        fake_pool['label'] = self.fake_eseries_pool_label
+        fake_pool['volumeGroupRef'] = 'foo'
+        fake_pools = [fake_pool]
+        fake_list_pools.return_value = fake_pools
+        wrong_eseries_pool_label = 'hostname@backend'
+        self.assertRaises(exception.NetAppDriverException,
+                          self.driver._create_volume, wrong_eseries_pool_label,
+                          self.fake_eseries_volume_label, self.fake_size_gb)
+
+    @mock.patch.object(driver_log, 'info')
+    @mock.patch.object(client.RestClient, 'list_storage_pools')
+    @mock.patch.object(client.RestClient, 'create_volume',
+                       mock.MagicMock(return_value='CorrectVolume'))
+    def test_helper_create_volume(self, storage_pools, log_info):
+        fake_pool = {}
+        fake_pool['label'] = self.fake_eseries_pool_label
+        fake_pool['volumeGroupRef'] = 'foo'
+        fake_pools = [fake_pool]
+        storage_pools.return_value = fake_pools
+        drv = self.driver
+        storage_vol = drv.driver._create_volume(self.fake_eseries_pool_label,
+                                                self.fake_eseries_volume_label,
+                                                self.fake_size_gb)
+        log_info.assert_called_once_with("Created volume with label %s.",
+                                         self.fake_eseries_volume_label)
+        self.assertEqual('CorrectVolume', storage_vol)
+
+    @mock.patch.object(client.RestClient, 'list_storage_pools')
+    @mock.patch.object(client.RestClient, 'create_volume',
+                       mock.MagicMock(
+                           side_effect=exception.NetAppDriverException))
+    @mock.patch.object(driver_log, 'info', mock.Mock())
+    def test_create_volume_check_exception(self, fake_list_pools):
+        fake_pool = {}
+        fake_pool['label'] = self.fake_eseries_pool_label
+        fake_pool['volumeGroupRef'] = 'foo'
+        fake_pools = [fake_pool]
+        fake_list_pools.return_value = fake_pools
+        self.assertRaises(exception.NetAppDriverException,
+                          self.driver._create_volume,
+                          self.fake_eseries_pool_label,
+                          self.fake_eseries_volume_label, self.fake_size_gb)
index 9915a2331e87e80ce543795ecbc26d383cee7d8f..f059b627a9c63307460a804dc1218db3c2770dbd 100644 (file)
@@ -50,10 +50,11 @@ def create_configuration():
 
 
 class FakeVolume(object):
-    def __init__(self, size=0):
+    def __init__(self, host='', size=0):
         self.size = size
         self.id = hash(self)
         self.name = None
+        self.host = host
 
     def __getitem__(self, key):
         return self.__dict__[key]
@@ -110,10 +111,11 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
         """Tests volume creation from snapshot."""
         drv = self._driver
         mox = self.mox
-        volume = FakeVolume(1)
+        location = '127.0.0.1:/nfs'
+        host = 'hostname@backend#' + location
+        volume = FakeVolume(host, 1)
         snapshot = FakeSnapshot(1)
 
-        location = '127.0.0.1:/nfs'
         expected_result = {'provider_location': location}
         mox.StubOutWithMock(drv, '_clone_volume')
         mox.StubOutWithMock(drv, '_get_volume_location')
@@ -797,6 +799,10 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
         if location != "nfs://host/path/image-id":
             self.fail("Unexpected direct url.")
 
+    def test_get_pool(self):
+        pool = self._driver.get_pool({'provider_location': 'fake-share'})
+        self.assertEqual(pool, 'fake-share')
+
 
 class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase):
     """Test direct NetApp C Mode driver only and not inherit."""
@@ -820,37 +826,43 @@ class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase):
         extra_specs = {}
         mock_volume_extra_specs.return_value = extra_specs
         fake_share = 'localhost:myshare'
+        host = 'hostname@backend#' + fake_share
         with mock.patch.object(drv, '_ensure_shares_mounted'):
-            with mock.patch.object(drv, '_find_shares',
-                                   return_value=['localhost:myshare']):
-                with mock.patch.object(drv, '_do_create_volume'):
-                    volume_info = self._driver.create_volume(FakeVolume(1))
-                    self.assertEqual(volume_info.get('provider_location'),
-                                     fake_share)
+            with mock.patch.object(drv, '_do_create_volume'):
+                volume_info = self._driver.create_volume(FakeVolume(host, 1))
+                self.assertEqual(volume_info.get('provider_location'),
+                                 fake_share)
+
+    def test_create_volume_no_pool_specified(self):
+        drv = self._driver
+        drv.ssc_enabled = False
+        host = 'hostname@backend'  # missing pool
+        with mock.patch.object(drv, '_ensure_shares_mounted'):
+            self.assertRaises(exception.InvalidHost,
+                              self._driver.create_volume, FakeVolume(host, 1))
 
     @mock.patch.object(netapp_nfs, 'get_volume_extra_specs')
     def test_create_volume_with_qos_policy(self, mock_volume_extra_specs):
         drv = self._driver
         drv.ssc_enabled = False
         extra_specs = {'netapp:qos_policy_group': 'qos_policy_1'}
-        fake_volume = FakeVolume(1)
         fake_share = 'localhost:myshare'
+        host = 'hostname@backend#' + fake_share
+        fake_volume = FakeVolume(host, 1)
         fake_qos_policy = 'qos_policy_1'
         mock_volume_extra_specs.return_value = extra_specs
 
         with mock.patch.object(drv, '_ensure_shares_mounted'):
-            with mock.patch.object(drv, '_find_shares',
-                                   return_value=['localhost:myshare']):
-                with mock.patch.object(drv, '_do_create_volume'):
-                    with mock.patch.object(drv,
-                                           '_set_qos_policy_group_on_volume'
-                                           ) as mock_set_qos:
-                        volume_info = self._driver.create_volume(fake_volume)
-                        self.assertEqual(volume_info.get('provider_location'),
-                                         'localhost:myshare')
-                        mock_set_qos.assert_called_once_with(fake_volume,
-                                                             fake_share,
-                                                             fake_qos_policy)
+            with mock.patch.object(drv, '_do_create_volume'):
+                with mock.patch.object(drv,
+                                       '_set_qos_policy_group_on_volume'
+                                       ) as mock_set_qos:
+                    volume_info = self._driver.create_volume(fake_volume)
+                    self.assertEqual(volume_info.get('provider_location'),
+                                     'localhost:myshare')
+                    mock_set_qos.assert_called_once_with(fake_volume,
+                                                         fake_share,
+                                                         fake_qos_policy)
 
     def test_copy_img_to_vol_copyoffload_success(self):
         drv = self._driver
@@ -1089,6 +1101,14 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase):
 
         return mox
 
+    def test_create_volume_no_pool_specified(self):
+        drv = self._driver
+        drv.ssc_enabled = False
+        host = 'hostname@backend'  # missing pool
+        with mock.patch.object(drv, '_ensure_shares_mounted'):
+            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")
@@ -1196,3 +1216,7 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase):
                 raise
 
         mox.VerifyAll()
+
+    def test_get_pool(self):
+        pool = self._driver.get_pool({'provider_location': 'fake-share'})
+        self.assertEqual(pool, 'fake-share')
index 04ded7fc15e2a9fa0af113f14542440f9ea2fc9a..40e077872d64a17140aeb2982629a540763965d9 100644 (file)
@@ -20,30 +20,83 @@ import uuid
 
 import mock
 
+from cinder import exception
 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
 
 
 class NetAppDirectISCSIDriverTestCase(test.TestCase):
 
     def setUp(self):
         super(NetAppDirectISCSIDriverTestCase, self).setUp()
+        configuration = self._set_config(create_configuration())
         self.driver = ntap_iscsi.NetAppDirectISCSIDriver(
-            configuration=mock.Mock())
+            configuration=configuration)
         self.driver.client = mock.Mock()
         self.fake_volume = str(uuid.uuid4())
         self.fake_lun = str(uuid.uuid4())
         self.fake_size = '1024'
-        self.fake_metadata = {
-            'OsType': 'linux',
-            'SpaceReserved': 'true',
-        }
+        self.fake_metadata = {'OsType': 'linux', 'SpaceReserved': 'true'}
         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(na_utils, '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)
+
+    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
+
     def test_create_lun(self):
         expected_path = '/vol/%s/%s' % (self.fake_volume, self.fake_lun)
 
@@ -89,6 +142,20 @@ class NetAppDirectISCSIDriverTestCase(test.TestCase):
             self.driver.client.invoke_successfully.assert_called_once_with(
                 mock.ANY, True)
 
+    def test_create_lun_raises_on_failure(self):
+        self.driver.client.invoke_successfully = mock.Mock(
+            side_effect=ntapi.NaApiError)
+        self.assertRaises(ntapi.NaApiError,
+                          self.driver.create_lun,
+                          self.fake_volume,
+                          self.fake_lun,
+                          self.fake_size,
+                          self.fake_metadata)
+
+    def test_update_volume_stats_is_abstract(self):
+        self.assertRaises(NotImplementedError,
+                          self.driver._update_volume_stats)
+
 
 class NetAppiSCSICModeTestCase(test.TestCase):
     """Test case for NetApp's C-Mode iSCSI driver."""
@@ -99,6 +166,7 @@ class NetAppiSCSICModeTestCase(test.TestCase):
             configuration=mock.Mock())
         self.driver.client = mock.Mock()
         self.driver.vserver = mock.Mock()
+        self.driver.ssc_vols = None
 
     def tearDown(self):
         super(NetAppiSCSICModeTestCase, self).tearDown()
@@ -182,6 +250,13 @@ class NetAppiSCSICModeTestCase(test.TestCase):
 
         self.assertEqual(1, self.driver.client.invoke_successfully.call_count)
 
+    @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)
+
 
 class NetAppiSCSI7ModeTestCase(test.TestCase):
     """Test case for NetApp's 7-Mode iSCSI driver."""
@@ -284,3 +359,10 @@ class NetAppiSCSI7ModeTestCase(test.TestCase):
         self.driver._clone_lun('fakeLUN', 'newFakeLUN')
 
         self.assertEqual(1, self.driver.client.invoke_successfully.call_count)
+
+    @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)
diff --git a/cinder/tests/volume/drivers/netapp/test_utils.py b/cinder/tests/volume/drivers/netapp/test_utils.py
new file mode 100644 (file)
index 0000000..38c8051
--- /dev/null
@@ -0,0 +1,59 @@
+# Copyright (c) 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.
+"""
+Mock unit tests for the NetApp driver utility module
+"""
+
+from cinder import test
+import cinder.volume.drivers.netapp.utils as na_utils
+
+
+class NetAppDriverUtilsTestCase(test.TestCase):
+
+    def test_to_bool(self):
+        self.assertTrue(na_utils.to_bool(True))
+        self.assertTrue(na_utils.to_bool('true'))
+        self.assertTrue(na_utils.to_bool('yes'))
+        self.assertTrue(na_utils.to_bool('y'))
+        self.assertTrue(na_utils.to_bool(1))
+        self.assertTrue(na_utils.to_bool('1'))
+        self.assertFalse(na_utils.to_bool(False))
+        self.assertFalse(na_utils.to_bool('false'))
+        self.assertFalse(na_utils.to_bool('asdf'))
+        self.assertFalse(na_utils.to_bool('no'))
+        self.assertFalse(na_utils.to_bool('n'))
+        self.assertFalse(na_utils.to_bool(0))
+        self.assertFalse(na_utils.to_bool('0'))
+        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_convert_es_fmt_to_uuid(self):
+        value = '4Z7JGGVS5VEJBE4LHLGGUUL7VQ'
+        result = str(na_utils.convert_es_fmt_to_uuid(value))
+        self.assertEqual(result, 'e67e931a-b2ed-4890-938b-3acc6a517fac')
+
+    def test_round_down(self):
+        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)
+        self.assertAlmostEqual(na_utils.round_down(0, '0.00'), 0)
+        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)
index 0d476f5e32ef425e31d9a59a8933346c00b6b470..1b1f97c7b0674ce0787ff6f252cef8f7af333e99 100644 (file)
@@ -21,6 +21,7 @@ import time
 import uuid
 
 from oslo.config import cfg
+import six
 
 from cinder import exception
 from cinder.i18n import _
@@ -35,6 +36,7 @@ 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 import utils as volume_utils
 
 
 LOG = logging.getLogger(__name__)
@@ -64,7 +66,7 @@ class Driver(driver.ISCSIDriver):
         self.configuration.append_config_values(netapp_connection_opts)
         self.configuration.append_config_values(netapp_transport_opts)
         self.configuration.append_config_values(netapp_eseries_opts)
-        self._objects = {'disk_pool_refs': [],
+        self._objects = {'disk_pool_refs': [], 'pools': [],
                          'volumes': {'label_ref': {}, 'ref_vol': {}},
                          'snapshots': {'label_ref': {}, 'ref_snap': {}}}
 
@@ -182,6 +184,7 @@ class Driver(driver.ISCSIDriver):
             if (pool.get('raidLevel') == 'raidDiskPool'
                     and pool['label'].lower() in pools):
                 self._objects['disk_pool_refs'].append(pool['volumeGroupRef'])
+                self._objects['pools'].append(pool)
 
     def _cache_volume(self, obj):
         """Caches volumes for further reference."""
@@ -284,14 +287,67 @@ class Driver(driver.ISCSIDriver):
                 return True
         return False
 
+    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.
+        """
+        eseries_volume = self._get_volume(volume['id'])
+        for pool in self._objects['pools']:
+            if pool['volumeGroupRef'] == eseries_volume['volumeGroupRef']:
+                return pool['label']
+        return None
+
     def create_volume(self, volume):
         """Creates a volume."""
-        label = utils.convert_uuid_to_es_fmt(volume['id'])
+
+        LOG.debug('create_volume on %s' % volume['host'])
+
+        # get E-series pool label as pool name
+        eseries_pool_label = volume_utils.extract_host(volume['host'],
+                                                       level='pool')
+
+        if eseries_pool_label is None:
+            msg = _("Pool is not available in the volume host field.")
+            raise exception.InvalidHost(reason=msg)
+
+        eseries_volume_label = utils.convert_uuid_to_es_fmt(volume['id'])
+
+        # get size of the requested volume creation
         size_gb = int(volume['size'])
-        vol = self._create_volume(label, size_gb)
+        vol = self._create_volume(eseries_pool_label, eseries_volume_label,
+                                  size_gb)
         self._cache_volume(vol)
 
-    def _create_volume(self, label, size_gb):
+    def _create_volume(self, eseries_pool_label, eseries_volume_label,
+                       size_gb):
+        """Creates volume with given label and size."""
+
+        target_pool = None
+
+        pools = self._client.list_storage_pools()
+        for pool in pools:
+            if pool["label"] == eseries_pool_label:
+                target_pool = pool
+                break
+
+        if not target_pool:
+            msg = _("Pools %s does not exist")
+            raise exception.NetAppDriverException(msg % eseries_pool_label)
+
+        try:
+            vol = self._client.create_volume(target_pool['volumeGroupRef'],
+                                             eseries_volume_label, size_gb)
+            LOG.info(_("Created volume with label %s."), eseries_volume_label)
+        except exception.NetAppDriverException as e:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_("Error creating volume. Msg - %s."),
+                          six.text_type(e))
+
+        return vol
+
+    def _schedule_and_create_volume(self, label, size_gb):
         """Creates volume with given label and size."""
         avl_pools = self._get_sorted_avl_storage_pools(size_gb)
         for pool in avl_pools:
@@ -305,28 +361,11 @@ class Driver(driver.ISCSIDriver):
         msg = _("Failure creating volume %s.")
         raise exception.NetAppDriverException(msg % label)
 
-    def _get_sorted_avl_storage_pools(self, size_gb):
-        """Returns storage pools sorted on available capacity."""
-        size = size_gb * units.Gi
-        pools = self._client.list_storage_pools()
-        sorted_pools = sorted(pools, key=lambda x:
-                              (int(x.get('totalRaidedSpace', 0))
-                               - int(x.get('usedSpace', 0))), reverse=True)
-        avl_pools = [x for x in sorted_pools
-                     if (x['volumeGroupRef'] in
-                         self._objects['disk_pool_refs']) and
-                     (int(x.get('totalRaidedSpace', 0)) -
-                      int(x.get('usedSpace', 0) >= size))]
-        if not avl_pools:
-            msg = _("No storage pool found with available capacity %s.")
-            exception.NotFound(msg % size_gb)
-        return avl_pools
-
     def create_volume_from_snapshot(self, volume, snapshot):
         """Creates a volume from a snapshot."""
         label = utils.convert_uuid_to_es_fmt(volume['id'])
         size = volume['size']
-        dst_vol = self._create_volume(label, size)
+        dst_vol = self._schedule_and_create_volume(label, size)
         try:
             src_vol = None
             src_vol = self._create_snapshot_volume(snapshot['id'])
@@ -624,31 +663,48 @@ class Driver(driver.ISCSIDriver):
     def _update_volume_stats(self):
         """Update volume statistics."""
         LOG.debug("Updating volume stats.")
-        self._stats = self._stats or {}
-        netapp_backend = 'NetApp_ESeries'
-        backend_name = self.configuration.safe_get('volume_backend_name')
-        self._stats["volume_backend_name"] = (
-            backend_name or netapp_backend)
-        self._stats["vendor_name"] = 'NetApp'
-        self._stats["driver_version"] = '1.0'
-        self._stats["storage_protocol"] = 'iSCSI'
-        self._stats["total_capacity_gb"] = 0
-        self._stats["free_capacity_gb"] = 0
-        self._stats["reserved_percentage"] = 0
-        self._stats["QoS_support"] = False
-        self._update_capacity()
-        self._garbage_collect_tmp_vols()
+        data = dict()
+        netapp_backend = "NetApp_ESeries"
+        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"] = []
 
-    def _update_capacity(self):
-        """Get free and total appliance capacity in bytes."""
-        tot_bytes, used_bytes = 0, 0
         pools = self._client.list_storage_pools()
         for pool in pools:
-            if pool['volumeGroupRef'] in self._objects['disk_pool_refs']:
-                tot_bytes = tot_bytes + int(pool.get('totalRaidedSpace', 0))
-                used_bytes = used_bytes + int(pool.get('usedSpace', 0))
-        self._stats['free_capacity_gb'] = (tot_bytes - used_bytes) / units.Gi
-        self._stats['total_capacity_gb'] = tot_bytes / units.Gi
+            cinder_pool = {}
+            cinder_pool["pool_name"] = pool.get("label", 0)
+            cinder_pool["QoS_support"] = False
+            cinder_pool["reserved_percentage"] = 0
+            if pool["volumeGroupRef"] in self._objects["disk_pool_refs"]:
+                tot_bytes = int(pool.get("totalRaidedSpace", 0))
+                used_bytes = int(pool.get("usedSpace", 0))
+                cinder_pool["free_capacity_gb"] = ((tot_bytes - used_bytes) /
+                                                   units.Gi)
+                cinder_pool["total_capacity_gb"] = tot_bytes / units.Gi
+                data["pools"].append(cinder_pool)
+
+        self._stats = data
+        self._garbage_collect_tmp_vols()
+
+    def _get_sorted_avl_storage_pools(self, size_gb):
+        """Returns storage pools sorted on available capacity."""
+        size = size_gb * units.Gi
+        pools = self._client.list_storage_pools()
+        sorted_pools = sorted(pools, key=lambda x:
+                              (int(x.get('totalRaidedSpace', 0))
+                               - int(x.get('usedSpace', 0))), reverse=True)
+        avl_pools = [x for x in sorted_pools
+                     if (x['volumeGroupRef'] in
+                         self._objects['disk_pool_refs']) and
+                     (int(x.get('totalRaidedSpace', 0)) -
+                      int(x.get('usedSpace', 0) >= size))]
+        if not avl_pools:
+            msg = _("No storage pool found with available capacity %s.")
+            LOG.warn(msg % size_gb)
+        return avl_pools
 
     def extend_volume(self, volume, new_size):
         """Extend an existing volume to the new size."""
index 761c79d46e953a36bc8aa8e693dff109e3260edc..941478cd8f54a26931a990fcce5f0e9b069ef35a 100644 (file)
@@ -26,6 +26,8 @@ import sys
 import time
 import uuid
 
+import six
+
 from cinder import exception
 from cinder.i18n import _
 from cinder.openstack.common import excutils
@@ -44,10 +46,12 @@ 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 provide_ems
+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__)
@@ -150,23 +154,52 @@ class NetAppDirectISCSIDriver(driver.ISCSIDriver):
         self._get_lun_list()
         LOG.debug("Success getting LUN list from server")
 
-    def create_volume(self, volume):
-        """Driver entry point for creating a new volume."""
-        default_size = '104857600'  # 100 MB
-        gigabytes = 1073741824L  # 2^30
+    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']
-        if int(volume['size']) == 0:
-            size = default_size
-        else:
-            size = str(int(volume['size']) * gigabytes)
-        metadata = {}
-        metadata['OsType'] = 'linux'
-        metadata['SpaceReserved'] = 'true'
+        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)
-        self._create_lun_on_eligible_vol(name, size, metadata, extra_specs)
-        LOG.debug("Created LUN with name %s" % name)
+        qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \
+            if extra_specs else None
+
+        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, name, size, metadata))
+        self._add_lun_to_table(NetAppLun(handle, lun_name, size, metadata))
 
     def delete_volume(self, volume):
         """Driver entry point for destroying existing volumes."""
@@ -336,22 +369,29 @@ class NetAppDirectISCSIDriver(driver.ISCSIDriver):
         minor = res.get_child_content('minor-version')
         return (major, minor)
 
-    def _create_lun_on_eligible_vol(self, name, size, metadata,
-                                    extra_specs=None):
-        """Creates an actual lun on filer."""
-        raise NotImplementedError()
+    def create_lun(self, volume_name, lun_name, size,
+                   metadata, qos_policy_group=None):
+        """Issues API request for creating LUN on volume."""
 
-    def create_lun(self, volume, lun, size, metadata, qos_policy_group=None):
-        """Issues api request for creating lun on volume."""
-        path = '/vol/%s/%s' % (volume, lun)
+        path = '/vol/%s/%s' % (volume_name, lun_name)
         lun_create = NaElement.create_node_with_children(
             'lun-create-by-size',
-            **{'path': path, 'size': size,
+            **{'path': path, 'size': six.text_type(size),
                 'ostype': metadata['OsType'],
                 'space-reservation-enabled': metadata['SpaceReserved']})
         if qos_policy_group:
             lun_create.add_new_child('qos-policy-group', qos_policy_group)
-        self.client.invoke_successfully(lun_create, True)
+
+        try:
+            self.client.invoke_successfully(lun_create, True)
+        except NaApiError as ex:
+            with excutils.save_and_reraise_exception():
+                msg = _("Error provisioning volume %(lun_name)s on "
+                        "%(volume_name)s. Details: %(ex)s")
+                msg_args = {'lun_name': lun_name,
+                            'volume_name': volume_name,
+                            'ex': six.text_type(ex)}
+                LOG.error(msg % msg_args)
 
     def _get_iscsi_service_details(self):
         """Returns iscsi iqn."""
@@ -769,47 +809,15 @@ class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver):
         ssc_utils.check_ssc_api_permissions(self.client)
         super(NetAppDirectCmodeISCSIDriver, self).check_for_setup_error()
 
-    def _create_lun_on_eligible_vol(self, name, size, metadata,
-                                    extra_specs=None):
-        """Creates an actual lun on filer."""
-        req_size = float(size) *\
-            float(self.configuration.netapp_size_multiplier)
-        qos_policy_group = None
-        if extra_specs:
-            qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None)
-        volumes = self._get_avl_volumes(req_size, extra_specs)
-        if not volumes:
-            msg = _('Failed to get vol with required'
-                    ' size and extra specs for volume: %s')
-            raise exception.VolumeBackendAPIException(data=msg % name)
-        for volume in volumes:
-            try:
-                self.create_lun(volume.id['name'], name, size, metadata,
-                                qos_policy_group=qos_policy_group)
-                metadata['Path'] = '/vol/%s/%s' % (volume.id['name'], name)
-                metadata['Volume'] = volume.id['name']
-                metadata['Qtree'] = None
-                return
-            except NaApiError as ex:
-                msg = _("Error provisioning vol %(name)s on "
-                        "%(volume)s. Details: %(ex)s")
-                LOG.error(msg % {'name': name,
-                                 'volume': volume.id['name'],
-                                 'ex': ex})
-            finally:
-                self._update_stale_vols(volume=volume)
+    def create_lun(self, volume_name, lun_name, size,
+                   metadata, qos_policy_group=None):
+        """Creates a LUN, handling ONTAP differences as needed."""
 
-    def _get_avl_volumes(self, size, extra_specs=None):
-        """Get the available volume by size, extra_specs."""
-        result = []
-        volumes = ssc_utils.get_volumes_for_specs(
-            self.ssc_vols, extra_specs)
-        if volumes:
-            sorted_vols = sorted(volumes, reverse=True)
-            for vol in sorted_vols:
-                if int(vol.space['size_avl_bytes']) >= int(size):
-                    result.append(vol)
-        return result
+        super(NetAppDirectCmodeISCSIDriver, self).create_lun(
+            volume_name, lun_name, size, metadata, qos_policy_group)
+
+        self._update_stale_vols(
+            volume=ssc_utils.NetAppVolume(volume_name, self.vserver))
 
     def _get_target_details(self):
         """Gets the target portal details."""
@@ -1036,7 +1044,6 @@ class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver):
         """Creates lun metadata dictionary."""
         self._is_naelement(lun)
         meta_dict = {}
-        self._is_naelement(lun)
         meta_dict['Vserver'] = lun.get_child_content('vserver')
         meta_dict['Volume'] = lun.get_child_content('volume')
         meta_dict['Qtree'] = lun.get_child_content('qtree')
@@ -1054,64 +1061,73 @@ class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver):
             self.client.set_vserver(None)
 
     def _update_volume_stats(self):
-        """Retrieve stats info from volume group."""
+        """Retrieve stats info from vserver."""
 
-        LOG.debug("Updating volume stats")
+        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"] = '1.0'
-        data["storage_protocol"] = 'iSCSI'
-
-        data['total_capacity_gb'] = 0
-        data['free_capacity_gb'] = 0
-        data['reserved_percentage'] = 0
-        data['QoS_support'] = False
-        self._update_cluster_vol_stats(data)
-        provide_ems(self, self.client, data, netapp_backend)
+        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, data, netapp_backend)
         self._stats = data
 
-    def _update_cluster_vol_stats(self, data):
-        """Updates vol stats with cluster config."""
-        sync = True if self.ssc_vols is None else False
-        ssc_utils.refresh_cluster_ssc(self, self.client, self.vserver,
-                                      synchronous=sync)
-        if self.ssc_vols:
-            data['netapp_mirrored'] = 'true'\
-                if self.ssc_vols['mirrored'] else 'false'
-            data['netapp_unmirrored'] = 'true'\
-                if len(self.ssc_vols['all']) > len(self.ssc_vols['mirrored'])\
-                else 'false'
-            data['netapp_dedup'] = 'true'\
-                if self.ssc_vols['dedup'] else 'false'
-            data['netapp_nodedup'] = 'true'\
-                if len(self.ssc_vols['all']) > len(self.ssc_vols['dedup'])\
-                else 'false'
-            data['netapp_compression'] = 'true'\
-                if self.ssc_vols['compression'] else 'false'
-            data['netapp_nocompression'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['compression'])\
-                else 'false'
-            data['netapp_thin_provisioned'] = 'true'\
-                if self.ssc_vols['thin'] else 'false'
-            data['netapp_thick_provisioned'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['thin']) else 'false'
-            if self.ssc_vols['all']:
-                vol_max = max(self.ssc_vols['all'])
-                data['total_capacity_gb'] =\
-                    int(vol_max.space['size_total_bytes']) / units.Gi
-                data['free_capacity_gb'] =\
-                    int(vol_max.space['size_avl_bytes']) / units.Gi
-            else:
-                data['total_capacity_gb'] = 0
-                data['free_capacity_gb'] = 0
-        else:
-            LOG.warn(_("Cluster ssc is not updated. No volume stats found."))
+    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']
+            pool['netapp:qos_policy_group'] = vol.qos['qos_policy_group']
+
+            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):
@@ -1162,10 +1178,7 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
         self.vol_refresh_interval = 1800
         self.vol_refresh_running = False
         self.vol_refresh_voluntary = False
-        # Setting it infinite at set up
-        # This will not rule out backend from scheduling
-        self.total_gb = 'infinite'
-        self.free_gb = 'infinite'
+        self.root_volume_name = self._get_root_volume_name()
 
     def check_for_setup_error(self):
         """Check that the driver is working and can communicate."""
@@ -1181,19 +1194,13 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
             raise exception.VolumeBackendAPIException(data=msg)
         super(NetAppDirect7modeISCSIDriver, self).check_for_setup_error()
 
-    def _create_lun_on_eligible_vol(self, name, size, metadata,
-                                    extra_specs=None):
-        """Creates an actual lun on filer."""
-        req_size = float(size) *\
-            float(self.configuration.netapp_size_multiplier)
-        volume = self._get_avl_volume_by_size(req_size)
-        if not volume:
-            msg = _('Failed to get vol with required size for volume: %s')
-            raise exception.VolumeBackendAPIException(data=msg % name)
-        self.create_lun(volume['name'], name, size, metadata)
-        metadata['Path'] = '/vol/%s/%s' % (volume['name'], name)
-        metadata['Volume'] = volume['name']
-        metadata['Qtree'] = None
+    def create_lun(self, volume_name, lun_name, size,
+                   metadata, qos_policy_group=None):
+        """Creates a LUN, handling ONTAP differences as needed."""
+
+        super(NetAppDirect7modeISCSIDriver, self).create_lun(
+            volume_name, lun_name, size, metadata, qos_policy_group)
+
         self.vol_refresh_voluntary = True
 
     def _get_filer_volumes(self, volume=None):
@@ -1207,23 +1214,15 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
             return volumes.get_children()
         return []
 
-    def _get_avl_volume_by_size(self, size):
-        """Get the available volume by size."""
+    def _get_root_volume_name(self):
+        # switch to volume-get-root-name API when possible
         vols = self._get_filer_volumes()
         for vol in vols:
-            avl_size = vol.get_child_content('size-available')
-            state = vol.get_child_content('state')
-            if float(avl_size) >= float(size) and state == 'online':
-                avl_vol = dict()
-                avl_vol['name'] = vol.get_child_content('name')
-                avl_vol['block-type'] = vol.get_child_content('block-type')
-                avl_vol['type'] = vol.get_child_content('type')
-                avl_vol['size-available'] = avl_size
-                if self.volume_list:
-                    if avl_vol['name'] in self.volume_list:
-                        return avl_vol
-                elif self._get_vol_option(avl_vol['name'], 'root') != 'true':
-                    return avl_vol
+            volume_name = vol.get_child_content('name')
+            if self._get_vol_option(volume_name, 'root') == 'true':
+                return volume_name
+        LOG.warn(_('Could not determine root volume name '
+                   'on %s.') % self._get_owner())
         return None
 
     def _get_igroup_by_initiator(self, initiator):
@@ -1278,13 +1277,17 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
         result = self.client.invoke_successfully(iscsi_service_iter, True)
         return result.get_child_content('node-name')
 
-    def _create_lun_handle(self, metadata):
-        """Returns lun handle based on filer type."""
+    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 _get_lun_list(self):
@@ -1461,31 +1464,81 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
         """Creates lun metadata dictionary."""
         self._is_naelement(lun)
         meta_dict = {}
-        self._is_naelement(lun)
         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 status info from volume group."""
-        LOG.debug("Updating volume stats")
+        """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_iSCSI_7mode_direct')
-        data["vendor_name"] = 'NetApp'
-        data["driver_version"] = self.VERSION
-        data["storage_protocol"] = 'iSCSI'
-        data['reserved_percentage'] = 0
-        data['QoS_support'] = False
-        self._get_capacity_info(data)
-        provide_ems(self, self.client, data, netapp_backend,
-                    server_type="7mode")
+        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, data, netapp_backend,
+                             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(
@@ -1498,8 +1551,9 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
                 bs = bs - 1
         return bs
 
-    def _get_capacity_info(self, data):
-        """Calculates the capacity information for the filer."""
+    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)):
@@ -1510,38 +1564,13 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
                         _("Volume refresh job already running. Returning..."))
                     return
                 self.vol_refresh_voluntary = False
-                self._refresh_capacity_info()
+                self.vols = self._get_filer_volumes()
                 self.vol_refresh_time = timeutils.utcnow()
             except Exception as e:
-                LOG.warn(_("Error refreshing vol capacity. Message: %s"), e)
+                LOG.warn(_("Error refreshing volume info. Message: %s"),
+                         six.text_type(e))
             finally:
                 set_safe_attr(self, 'vol_refresh_running', False)
-        data['total_capacity_gb'] = self.total_gb
-        data['free_capacity_gb'] = self.free_gb
-
-    def _refresh_capacity_info(self):
-        """Gets the latest capacity information."""
-        LOG.info(_("Refreshing capacity info for %s."), self.client)
-        total_bytes = 0
-        free_bytes = 0
-        vols = self._get_filer_volumes()
-        for vol in vols:
-            volume = vol.get_child_content('name')
-            if self.volume_list and volume not in self.volume_list:
-                continue
-            state = vol.get_child_content('state')
-            inconsistent = vol.get_child_content('is-inconsistent')
-            invalid = vol.get_child_content('is-invalid')
-            if (state == 'online' and inconsistent == 'false'
-                    and invalid == 'false'):
-                total_size = vol.get_child_content('size-total')
-                if total_size:
-                    total_bytes = total_bytes + int(total_size)
-                avl_size = vol.get_child_content('size-available')
-                if avl_size:
-                    free_bytes = free_bytes + int(avl_size)
-        self.total_gb = total_bytes / units.Gi
-        self.free_gb = free_bytes / units.Gi
 
     def delete_volume(self, volume):
         """Driver entry point for destroying existing volumes."""
index d16fd7568864385ad45bdb1d5bddc42ffc68cf80..5980c4a949182b27c1ec1da58d698307d1d56f63 100644 (file)
@@ -23,6 +23,7 @@ from threading import Timer
 import time
 import uuid
 
+import six
 import six.moves.urllib.parse as urlparse
 
 from cinder import exception
@@ -45,9 +46,9 @@ 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 provide_ems
 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__)
@@ -81,6 +82,14 @@ class NetAppNFSDriver(nfs.NfsDriver):
         """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
@@ -217,8 +226,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
 
     def _update_volume_stats(self):
         """Retrieve stats info from volume group."""
-        super(NetAppNFSDriver, self)._update_volume_stats()
-        self._spawn_clean_cache_job()
+        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."""
@@ -719,6 +727,23 @@ class NetAppDirectNfsDriver (NetAppNFSDriver):
             '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."""
@@ -773,34 +798,39 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
 
         :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 = None
-        if extra_specs:
-            qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None)
-        eligible = self._find_shares(volume['size'], extra_specs)
-        if not eligible:
-            raise exception.NfsNoSuitableShareFound(
-                volume_size=volume['size'])
-        for sh in eligible:
-            try:
-                volume['provider_location'] = sh
-                LOG.info(_('casted to %s') % volume['provider_location'])
-                self._do_create_volume(volume)
-                if qos_policy_group:
-                    self._set_qos_policy_group_on_volume(volume, sh,
-                                                         qos_policy_group)
-                return {'provider_location': volume['provider_location']}
-            except Exception as ex:
-                LOG.error(_("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(sh))
+        qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \
+            if extra_specs else None
+
+        try:
+            volume['provider_location'] = share
+            LOG.info(_('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(_("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']))
 
@@ -817,23 +847,6 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
                'vserver': self.vserver})
         self._invoke_successfully(file_assign_qos)
 
-    def _find_shares(self, size, extra_specs):
-        """Finds suitable shares for given params."""
-        shares = []
-        containers = []
-        if self.ssc_enabled:
-            vols = ssc_utils.get_volumes_for_specs(self.ssc_vols, extra_specs)
-            containers = [x.export['path'] for x in vols]
-        else:
-            containers = self._mounted_shares
-        for sh in containers:
-            if self._is_share_eligible(sh, size):
-                size, avl, alloc = self._get_capacity_info(sh)
-                shares.append((sh, avl))
-        shares = [a for a, b in sorted(
-            shares, key=lambda x: x[1], reverse=True)]
-        return shares
-
     def _clone_volume(self, volume_name, clone_name,
                       volume_id, share=None):
         """Clones mounted volume on NetApp Cluster."""
@@ -927,58 +940,85 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
         self._invoke_successfully(clone_create, vserver)
 
     def _update_volume_stats(self):
-        """Retrieve stats info from volume group."""
-        super(NetAppDirectCmodeNfsDriver, self)._update_volume_stats()
-        netapp_backend = 'NetApp_NFS_cluster_direct'
+        """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')
-        self._stats["volume_backend_name"] = (backend_name or
-                                              netapp_backend)
-        self._stats["vendor_name"] = 'NetApp'
-        self._stats["driver_version"] = '1.0'
-        self._update_cluster_vol_stats(self._stats)
-        provide_ems(self, self._client, self._stats, netapp_backend)
-
-    def _update_cluster_vol_stats(self, data):
-        """Updates vol stats with cluster config."""
-        if self.ssc_enabled:
-            sync = True if self.ssc_vols is None else False
-            ssc_utils.refresh_cluster_ssc(self, self._client, self.vserver,
-                                          synchronous=sync)
-        else:
-            LOG.warn(_("No vserver set in config. SSC will be disabled."))
-        if self.ssc_vols:
-            data['netapp_mirrored'] = 'true'\
-                if self.ssc_vols['mirrored'] else 'false'
-            data['netapp_unmirrored'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['mirrored']) else 'false'
-            data['netapp_dedup'] = 'true'\
-                if self.ssc_vols['dedup'] else 'false'
-            data['netapp_nodedup'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['dedup']) else 'false'
-            data['netapp_compression'] = 'true'\
-                if self.ssc_vols['compression'] else 'false'
-            data['netapp_nocompression'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['compression']) else 'false'
-            data['netapp_thin_provisioned'] = 'true'\
-                if self.ssc_vols['thin'] else 'false'
-            data['netapp_thick_provisioned'] = 'true'\
-                if len(self.ssc_vols['all']) >\
-                len(self.ssc_vols['thin']) else 'false'
-            if self.ssc_vols['all']:
-                vol_max = max(self.ssc_vols['all'])
-                data['total_capacity_gb'] =\
-                    int(vol_max.space['size_total_bytes']) / units.Gi
-                data['free_capacity_gb'] =\
-                    int(vol_max.space['size_avl_bytes']) / units.Gi
-            else:
-                data['total_capacity_gb'] = 0
-                data['free_capacity_gb'] = 0
-        elif self.ssc_enabled:
-            LOG.warn(_("No cluster ssc stats found."
-                       " Wait for next volume stats update."))
+        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, data, netapp_backend)
+        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']
+                pool['netapp:qos_policy_group'] = vol.qos['qos_policy_group']
+
+                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):
@@ -1323,6 +1363,39 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver):
         result = server.invoke_successfully(na_element, True)
         return result
 
+    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(_('Creating volume at location %s')
+                 % volume['provider_location'])
+
+        try:
+            self._do_create_volume(volume)
+        except Exception as ex:
+            LOG.error(_("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."""
@@ -1420,16 +1493,58 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver):
             retry = retry - 1
 
     def _update_volume_stats(self):
-        """Retrieve stats info from volume group."""
-        super(NetAppDirect7modeNfsDriver, self)._update_volume_stats()
+        """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')
-        self._stats["volume_backend_name"] = (backend_name or
-                                              'NetApp_NFS_7mode_direct')
-        self._stats["vendor_name"] = 'NetApp'
-        self._stats["driver_version"] = self.VERSION
-        provide_ems(self, self._client, self._stats, netapp_backend,
-                    server_type="7mode")
+        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, data, netapp_backend,
+                             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."""
index d70a8dbd3ca507f44753061ff699ca4f7f09d3cd..a318b10768ebac85a2e980b48b0c15b4aaa60aef 100644 (file)
@@ -20,6 +20,8 @@ Storage service catalog utility functions and classes for NetApp systems.
 import copy
 from threading import Timer
 
+import six
+
 from cinder import exception
 from cinder.i18n import _
 from cinder.openstack.common import log as logging
@@ -240,7 +242,7 @@ def create_vol_list(vol_attrs):
             vols.add(vol)
         except KeyError as e:
             LOG.debug('Unexpected error while creating'
-                      ' ssc vol list. Message - %s' % (e.message))
+                      ' ssc vol list. Message - %s' % six.text_type(e))
             continue
     return vols
 
index 7b74c3e7391d1b467ecf9805a2b7bd53d60ecb0e..10cea03e6dfd1c1ca44fa81911922a01849a2018 100644 (file)
@@ -23,9 +23,12 @@ NetApp drivers to achieve the desired functionality.
 import base64
 import binascii
 import copy
+import decimal
 import socket
 import uuid
 
+import six
+
 from cinder import context
 from cinder import exception
 from cinder.i18n import _
@@ -356,3 +359,8 @@ 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))