from copy import deepcopy
import sys
+import ddt
import mock
from oslo_utils import units
"total": 0,
}
+PERF_INFO = {
+ 'writes_per_sec': 318,
+ 'usec_per_write_op': 255,
+ 'output_per_sec': 234240,
+ 'reads_per_sec': 15,
+ 'input_per_sec': 2827943,
+ 'time': '2015-12-17T21:50:55Z',
+ 'usec_per_read_op': 192,
+ 'queue_depth': 4,
+}
+PERF_INFO_RAW = [PERF_INFO]
+
ISCSI_CONNECTION_INFO = {
"driver_volume_type": "iscsi",
"data": {
"size": 3221225472,
"source": "vol1"
}
+PURE_PGROUP = {
+ "hgroups": None,
+ "hosts": None,
+ "name": "pg1",
+ "source": "pure01",
+ "targets": None,
+ "volumes": ["v1"]
+}
class FakePureStorageHTTPError(Exception):
mock_func.side_effect = original_side_effect
-class PureBaseVolumeDriverTestCase(PureDriverTestCase):
+class PureBaseSharedDriverTestCase(PureDriverTestCase):
def setUp(self):
- super(PureBaseVolumeDriverTestCase, self).setUp()
+ super(PureBaseSharedDriverTestCase, self).setUp()
self.driver = pure.PureBaseVolumeDriver(configuration=self.mock_config)
-
self.driver._array = self.array
self.array.get_rest_version.return_value = '1.4'
+
+class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
+ def setUp(self):
+ super(PureBaseVolumeDriverTestCase, self).setUp()
+
def test_generate_purity_host_name(self):
result = self.driver._generate_purity_host_name(
"really-long-string-thats-a-bit-too-long")
private=True)
self.array.delete_host.assert_called_once_with(PURE_HOST_NAME)
- @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
- @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
- def test_get_volume_stats(self, mock_space, mock_filter):
- filter_function = "capabilities.total_volumes < 10"
- mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
- mock_filter.return_value = filter_function
- self.assertEqual({}, self.driver.get_volume_stats())
- self.array.get.return_value = SPACE_INFO
- result = {
- "volume_backend_name": VOLUME_BACKEND_NAME,
- "vendor_name": "Pure Storage",
- "driver_version": self.driver.VERSION,
- "storage_protocol": None,
- "total_capacity_gb": TOTAL_CAPACITY,
- "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
- "reserved_percentage": 0,
- "consistencygroup_support": True,
- "thin_provisioning_support": True,
- "provisioned_capacity": PROVISIONED_CAPACITY,
- "max_over_subscription_ratio": (PROVISIONED_CAPACITY /
- USED_SPACE),
- "total_volumes": 100,
- "filter_function": filter_function,
- "multiattach": True,
- }
- real_result = self.driver.get_volume_stats(refresh=True)
- self.assertDictMatch(result, real_result)
- self.assertDictMatch(result, self.driver._stats)
-
- @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
- @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
- def test_get_volume_stats_empty_array(self, mock_space, mock_filter):
- filter_function = "capabilities.total_volumes < 10"
- mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
- mock_filter.return_value = filter_function
- self.assertEqual({}, self.driver.get_volume_stats())
- self.array.get.return_value = SPACE_INFO_EMPTY
- result = {
- "volume_backend_name": VOLUME_BACKEND_NAME,
- "vendor_name": "Pure Storage",
- "driver_version": self.driver.VERSION,
- "storage_protocol": None,
- "total_capacity_gb": TOTAL_CAPACITY,
- "free_capacity_gb": TOTAL_CAPACITY,
- "reserved_percentage": 0,
- "consistencygroup_support": True,
- "thin_provisioning_support": True,
- "provisioned_capacity": PROVISIONED_CAPACITY,
- "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
- "total_volumes": 100,
- "filter_function": filter_function,
- "multiattach": True,
- }
- real_result = self.driver.get_volume_stats(refresh=True)
- self.assertDictMatch(result, real_result)
- self.assertDictMatch(result, self.driver._stats)
-
- @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
- @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
- def test_get_volume_stats_nothing_provisioned(self, mock_space,
- mock_filter):
- filter_function = "capabilities.total_volumes < 10"
- mock_space.return_value = (0, 0)
- mock_filter.return_value = filter_function
- self.assertEqual({}, self.driver.get_volume_stats())
- self.array.get.return_value = SPACE_INFO
- result = {
- "volume_backend_name": VOLUME_BACKEND_NAME,
- "vendor_name": "Pure Storage",
- "driver_version": self.driver.VERSION,
- "storage_protocol": None,
- "total_capacity_gb": TOTAL_CAPACITY,
- "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
- "reserved_percentage": 0,
- "consistencygroup_support": True,
- "thin_provisioning_support": True,
- "provisioned_capacity": 0,
- "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
- "total_volumes": 0,
- "filter_function": filter_function,
- "multiattach": True,
- }
- real_result = self.driver.get_volume_stats(refresh=True)
- self.assertDictMatch(result, real_result)
- self.assertDictMatch(result, self.driver._stats)
-
def test_extend_volume(self):
vol_name = VOLUME["name"] + "-cinder"
self.driver.extend_volume(VOLUME, 3)
self.driver._connect, VOLUME, FC_CONNECTOR)
self.assertTrue(self.array.connect_host.called)
self.assertTrue(self.array.list_volume_private_connections)
+
+
+@ddt.ddt
+class PureVolumeUpdateStatsTestCase(PureBaseSharedDriverTestCase):
+ def setUp(self):
+ super(PureVolumeUpdateStatsTestCase, self).setUp()
+ self.array.get.side_effect = self.fake_get_array
+
+ def fake_get_array(*args, **kwargs):
+ if 'action' in kwargs and kwargs['action'] is 'monitor':
+ return PERF_INFO_RAW
+
+ if 'space' in kwargs and kwargs['space'] is True:
+ return SPACE_INFO
+
+ @ddt.data(dict(used=10,
+ provisioned=100,
+ config_ratio=5,
+ expected_ratio=5,
+ auto=False),
+ dict(used=10,
+ provisioned=100,
+ config_ratio=5,
+ expected_ratio=10,
+ auto=True),
+ dict(used=0,
+ provisioned=100,
+ config_ratio=5,
+ expected_ratio=5,
+ auto=True),
+ dict(used=10,
+ provisioned=0,
+ config_ratio=5,
+ expected_ratio=5,
+ auto=True))
+ @ddt.unpack
+ def test_get_thin_provisioning(self,
+ used,
+ provisioned,
+ config_ratio,
+ expected_ratio,
+ auto):
+ self.mock_config.pure_automatic_max_oversubscription_ratio = auto
+ self.mock_config.max_over_subscription_ratio = config_ratio
+ actual_ratio = self.driver._get_thin_provisioning(provisioned, used)
+ self.assertEqual(expected_ratio, actual_ratio)
+
+ @mock.patch(BASE_DRIVER_OBJ + '.get_goodness_function')
+ @mock.patch(BASE_DRIVER_OBJ + '.get_filter_function')
+ @mock.patch(BASE_DRIVER_OBJ + '._get_provisioned_space')
+ @mock.patch(BASE_DRIVER_OBJ + '._get_thin_provisioning')
+ def test_get_volume_stats(self, mock_get_thin_provisioning, mock_get_space,
+ mock_get_filter, mock_get_goodness):
+ filter_function = 'capabilities.total_volumes < 10'
+ goodness_function = '90'
+ num_hosts = 20
+ num_snaps = 175
+ num_pgroups = 15
+ reserved_percentage = 12
+
+ self.array.list_hosts.return_value = [PURE_HOST] * num_hosts
+ self.array.list_volumes.return_value = [PURE_SNAPSHOT] * num_snaps
+ self.array.list_pgroups.return_value = [PURE_PGROUP] * num_pgroups
+ self.mock_config.reserved_percentage = reserved_percentage
+ mock_get_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
+ mock_get_filter.return_value = filter_function
+ mock_get_goodness.return_value = goodness_function
+ mock_get_thin_provisioning.return_value = (PROVISIONED_CAPACITY /
+ USED_SPACE)
+
+ expected_result = {
+ 'volume_backend_name': VOLUME_BACKEND_NAME,
+ 'vendor_name': 'Pure Storage',
+ 'driver_version': self.driver.VERSION,
+ 'storage_protocol': None,
+ 'consistencygroup_support': True,
+ 'thin_provisioning_support': True,
+ 'multiattach': True,
+ 'total_capacity_gb': TOTAL_CAPACITY,
+ 'free_capacity_gb': TOTAL_CAPACITY - USED_SPACE,
+ 'reserved_percentage': reserved_percentage,
+ 'provisioned_capacity': PROVISIONED_CAPACITY,
+ 'max_over_subscription_ratio': (PROVISIONED_CAPACITY /
+ USED_SPACE),
+ 'filter_function': filter_function,
+ 'goodness_function': goodness_function,
+ 'total_volumes': 100,
+ 'total_snapshots': num_snaps,
+ 'total_hosts': num_hosts,
+ 'total_pgroups': num_pgroups,
+ 'writes_per_sec': PERF_INFO['writes_per_sec'],
+ 'reads_per_sec': PERF_INFO['reads_per_sec'],
+ 'input_per_sec': PERF_INFO['input_per_sec'],
+ 'output_per_sec': PERF_INFO['output_per_sec'],
+ 'usec_per_read_op': PERF_INFO['usec_per_read_op'],
+ 'usec_per_write_op': PERF_INFO['usec_per_write_op'],
+ 'queue_depth': PERF_INFO['queue_depth'],
+ }
+
+ real_result = self.driver.get_volume_stats(refresh=True)
+ self.assertDictMatch(expected_result, real_result)
+
+ # Make sure when refresh=False we are using cached values and not
+ # sending additional requests to the array.
+ self.array.reset_mock()
+ real_result = self.driver.get_volume_stats(refresh=False)
+ self.assertDictMatch(expected_result, real_result)
+ self.assertFalse(self.array.get.called)
+ self.assertFalse(self.array.list_volumes.called)
+ self.assertFalse(self.array.list_hosts.called)
+ self.assertFalse(self.array.list_pgroups.called)
PURE_OPTS = [
cfg.StrOpt("pure_api_token",
help="REST API authorization token."),
+ cfg.BoolOpt("pure_automatic_max_oversubscription_ratio",
+ default=True,
+ help="Automatically determine an oversubscription ratio based "
+ "on the current total data reduction values. If used "
+ "this calculated value will override the "
+ "max_over_subscription_ratio config option.")
]
CONF = cfg.CONF
def _update_stats(self):
"""Set self._stats with relevant information."""
- info = self._array.get(space=True)
- total_capacity = float(info["capacity"]) / units.Gi
- used_space = float(info["total"]) / units.Gi
+
+ # Collect info from the array
+ space_info = self._array.get(space=True)
+ perf_info = self._array.get(action='monitor')[0] # Always first index
+ hosts = self._array.list_hosts()
+ snaps = self._array.list_volumes(snap=True, pending=True)
+ pgroups = self._array.list_pgroups(pending=True)
+
+ # Perform some translations and calculations
+ total_capacity = float(space_info["capacity"]) / units.Gi
+ used_space = float(space_info["total"]) / units.Gi
free_space = float(total_capacity - used_space)
prov_space, total_vols = self._get_provisioned_space()
+ total_hosts = len(hosts)
+ total_snaps = len(snaps)
+ total_pgroups = len(pgroups)
provisioned_space = float(prov_space) / units.Gi
- # If array is empty we can not calculate a max oversubscription ratio.
- # In this case we choose 20 as a default value for the ratio. Once
- # some volumes are actually created and some data is stored on the
- # array a much more accurate number will be presented based on current
- # usage.
- if used_space == 0 or provisioned_space == 0:
- thin_provisioning = 20
- else:
- thin_provisioning = provisioned_space / used_space
- data = {
- "volume_backend_name": self._backend_name,
- "vendor_name": "Pure Storage",
- "driver_version": self.VERSION,
- "storage_protocol": self._storage_protocol,
- "total_capacity_gb": total_capacity,
- "free_capacity_gb": free_space,
- "reserved_percentage": 0,
- "consistencygroup_support": True,
- "thin_provisioning_support": True,
- "provisioned_capacity": provisioned_space,
- "max_over_subscription_ratio": thin_provisioning,
- "total_volumes": total_vols,
- "filter_function": self.get_filter_function(),
- "multiattach": True,
- }
+ thin_provisioning = self._get_thin_provisioning(provisioned_space,
+ used_space)
+
+ # Start with some required info
+ data = dict(
+ volume_backend_name=self._backend_name,
+ vendor_name='Pure Storage',
+ driver_version=self.VERSION,
+ storage_protocol=self._storage_protocol,
+ )
+
+ # Add flags for supported features
+ data['consistencygroup_support'] = True
+ data['thin_provisioning_support'] = True
+ data['multiattach'] = True
+
+ # Add capacity info for scheduler
+ data['total_capacity_gb'] = total_capacity
+ data['free_capacity_gb'] = free_space
+ data['reserved_percentage'] = self.configuration.reserved_percentage
+ data['provisioned_capacity'] = provisioned_space
+ data['max_over_subscription_ratio'] = thin_provisioning
+
+ # Add the filtering/goodness functions
+ data['filter_function'] = self.get_filter_function()
+ data['goodness_function'] = self.get_goodness_function()
+
+ # Add array metadata counts for filtering and weighing functions
+ data['total_volumes'] = total_vols
+ data['total_snapshots'] = total_snaps
+ data['total_hosts'] = total_hosts
+ data['total_pgroups'] = total_pgroups
+
+ # Add performance stats for filtering and weighing functions
+ # IOPS
+ data['writes_per_sec'] = perf_info['writes_per_sec']
+ data['reads_per_sec'] = perf_info['reads_per_sec']
+
+ # Bandwidth
+ data['input_per_sec'] = perf_info['input_per_sec']
+ data['output_per_sec'] = perf_info['output_per_sec']
+
+ # Latency
+ data['usec_per_read_op'] = perf_info['usec_per_read_op']
+ data['usec_per_write_op'] = perf_info['usec_per_write_op']
+ data['queue_depth'] = perf_info['queue_depth']
+
self._stats = data
def _get_provisioned_space(self):
volumes = self._array.list_volumes(pending=True)
return sum(item["size"] for item in volumes), len(volumes)
+ def _get_thin_provisioning(self, provisioned_space, used_space):
+ """Get the current value for the thin provisioning ratio.
+
+ If pure_automatic_max_oversubscription_ratio is True we will calculate
+ a value, if not we will respect the configuration option for the
+ max_over_subscription_ratio.
+ """
+ if (self.configuration.pure_automatic_max_oversubscription_ratio and
+ used_space != 0 and provisioned_space != 0):
+ # If array is empty we can not calculate a max oversubscription
+ # ratio. In this case we look to the config option as a starting
+ # point. Once some volumes are actually created and some data is
+ # stored on the array a much more accurate number will be
+ # presented based on current usage.
+ thin_provisioning = provisioned_space / used_space
+ else:
+ thin_provisioning = self.configuration.max_over_subscription_ratio
+
+ return thin_provisioning
+
@log_debug_trace
def extend_volume(self, volume, new_size):
"""Extend volume to new_size."""
class PureISCSIDriver(PureBaseVolumeDriver, san.SanISCSIDriver):
- VERSION = "3.0.0"
+ VERSION = "4.0.0"
def __init__(self, *args, **kwargs):
execute = kwargs.pop("execute", utils.execute)
class PureFCDriver(PureBaseVolumeDriver, driver.FibreChannelDriver):
- VERSION = "1.0.0"
+ VERSION = "2.0.0"
def __init__(self, *args, **kwargs):
execute = kwargs.pop("execute", utils.execute)