Merge "Dell PowerScale: Added support of thin provisioning"

This commit is contained in:
Zuul
2025-08-12 15:57:40 +00:00
committed by Gerrit Code Review
7 changed files with 144 additions and 11 deletions

View File

@@ -284,7 +284,7 @@ More information: :ref:`capabilities_and_extra_specs`
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+--------------------------+
| EMC Unity | N | T | \- | \- | N | \- | \- | N | S | \- | P | Q | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+--------------------------+
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K | \- | \- | P | \- | \- | \- |
| EMC Isilon | \- | K | \- | \- | F | \- | \- | K | \- | \- | P | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+--------------------------+
| Dell EMC PowerStore | \- | B | \- | \- | B | \- | \- | B | B | \- | B | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+--------------------------+

View File

@@ -62,6 +62,16 @@ Systems service configuration file for the Isilon driver:
emc_nas_login = <username>
emc_nas_password = <password>
Thin Provisioning
~~~~~~~~~~~~~~~~~
PowerScale systems have thin provisioning enabled by default.
Add the parameter below to set an advisory limit.
.. code-block:: ini
powerscale_threshold_limit = <threshold percentage value>
Restrictions
~~~~~~~~~~~~

View File

@@ -33,8 +33,9 @@ from manila.share.drivers.dell_emc.plugins.isilon import isilon_api
1.0.0 - Fix Http auth issue, SSL verification error and etc
1.0.1 - Add support for update share stats
1.0.2 - Add support for ensure shares
1.0.3 - Add support for thin provisioning
"""
VERSION = "1.0.2"
VERSION = "1.0.3"
CONF = cfg.CONF
@@ -44,7 +45,11 @@ POWERSCALE_OPTS = [
cfg.StrOpt('powerscale_dir_permission',
default='0777',
help='Predefined ACL value or POSIX mode '
'for PowerScale directories.')
'for PowerScale directories.'),
cfg.IntOpt('powerscale_threshold_limit',
default=0,
help='Specifies the threshold limit (in percentage) '
'for triggering SmartQuotas alerts in PowerScale')
]
@@ -78,6 +83,7 @@ class IsilonStorageConnection(base.StorageConnection):
self.reserved_snapshot_percentage = None
self.reserved_share_extend_percentage = None
self.max_over_subscription_ratio = None
self._threshold_limit = 0
def _get_container_path(self, share):
"""Return path to a container."""
@@ -298,6 +304,7 @@ class IsilonStorageConnection(base.StorageConnection):
self._username = config.safe_get("emc_nas_login")
self._password = config.safe_get("emc_nas_password")
self._root_dir = config.safe_get("emc_nas_root_dir")
self._threshold_limit = config.safe_get("powerscale_threshold_limit")
# validate IP, username and password
if not all([self._server,
@@ -316,7 +323,8 @@ class IsilonStorageConnection(base.StorageConnection):
self._isilon_api = isilon_api.IsilonApi(
self._server_url, self._username, self._password,
self._verify_ssl_cert, self._ssl_cert_path,
self._dir_permission)
self._dir_permission,
self._threshold_limit)
if not self._isilon_api.is_path_existent(self._root_dir):
self._create_directory(self._root_dir, recursive=True)
@@ -344,7 +352,6 @@ class IsilonStorageConnection(base.StorageConnection):
"""Retrieve stats info from share."""
stats_dict['driver_version'] = VERSION
stats_dict['storage_protocol'] = 'NFS_CIFS'
# PowerScale does not support pools.
# To align with manila scheduler 'pool-aware' strategic,
# report with one pool structure.
@@ -357,13 +364,15 @@ class IsilonStorageConnection(base.StorageConnection):
'reserved_share_extend_percentage':
self.reserved_share_extend_percentage,
'max_over_subscription_ratio':
self.max_over_subscription_ratio
self.max_over_subscription_ratio,
'thin_provisioning': True,
}
spaces = self._isilon_api.get_space_stats()
if spaces:
pool_stat['total_capacity_gb'] = spaces['total'] // units.Gi
pool_stat['free_capacity_gb'] = spaces['free'] // units.Gi
pool_stat['allocated_capacity_gb'] = spaces['used'] // units.Gi
allocated_space = self._isilon_api.get_allocated_space()
pool_stat['allocated_capacity_gb'] = allocated_space
stats_dict['pools'] = [pool_stat]

View File

@@ -32,7 +32,8 @@ class IsilonApi(object):
def __init__(self, api_url, username, password,
verify_ssl_cert=False,
ssl_cert_path=None,
dir_permission=None):
dir_permission=None,
threshold_limit=0):
self.host_url = api_url
self.session = requests.session()
self.username = username
@@ -40,6 +41,7 @@ class IsilonApi(object):
self.verify_ssl_cert = verify_ssl_cert
self.certificate_path = ssl_cert_path
self.dir_permission = dir_permission
self.threshold_limit = threshold_limit
# Create session
self.session_token = None
@@ -279,6 +281,9 @@ class IsilonApi(object):
def quota_create(self, path, quota_type, size):
thresholds = {'hard': size}
if self.threshold_limit > 0:
advisory_size = round((size * self.threshold_limit) / 100)
thresholds['advisory'] = int(advisory_size)
data = {
'path': path,
'type': quota_type,
@@ -315,6 +320,9 @@ class IsilonApi(object):
def quota_modify_size(self, quota_id, new_size):
data = {'thresholds': {'hard': new_size}}
if self.threshold_limit > 0:
advisory_size = round((new_size * self.threshold_limit) / 100)
data.get('thresholds')['advisory'] = int(advisory_size)
response = self.send_put_request(
'{0}/platform/1/quota/quotas/{1}'.format(self.host_url, quota_id),
data=data
@@ -393,6 +401,22 @@ class IsilonApi(object):
spaces['used'] = stat['value']
return spaces
def get_allocated_space(self):
url = '{0}/platform/1/quota/quotas'.format(self.host_url)
r = self.send_get_request(url)
allocated_capacity = 0
if r.status_code != 200:
raise exception.ShareBackendException(
msg=_('Failed to get share quotas from PowerScale.')
)
quotas = r.json()['quotas']
for quota in quotas:
if quota['thresholds']['hard'] is not None:
allocated_capacity += quota['thresholds']['hard']
if allocated_capacity > 0:
return round(allocated_capacity / (1024 ** 3), 2)
return allocated_capacity
def get_cluster_version(self):
url = '{0}/platform/12/cluster/version'.format(self.host_url)
r = self.send_get_request(url)

View File

@@ -516,8 +516,8 @@ class IsilonTest(test.TestCase):
self._mock_isilon_api.get_space_stats.return_value = {
'total': 1000 * units.Gi,
'free': 100 * units.Gi,
'used': 1 * units.Gi,
}
self._mock_isilon_api.get_allocated_space.return_value = 2110.0
stats_dict = {'share_backend_name': 'PowerScale_backend'}
self.storage_connection.update_share_stats(stats_dict)
@@ -527,10 +527,11 @@ class IsilonTest(test.TestCase):
'reserved_snapshot_percentage': 0,
'reserved_share_extend_percentage': 0,
'max_over_subscription_ratio': None,
'thin_provisioning': True,
'total_capacity_gb': 1000,
'free_capacity_gb': 100,
'allocated_capacity_gb': 1,
'qos': False
'allocated_capacity_gb': 2110.0,
'qos': False,
}
expected_stats = {
'share_backend_name': 'PowerScale_backend',

View File

@@ -42,6 +42,11 @@ class IsilonApiTest(test.TestCase):
self._mock_url, self.username, self.password,
dir_permission=self.dir_permission
)
self.isilon_api_threshold = isilon_api.IsilonApi(
self._mock_url, self.username, self.password,
dir_permission=self.dir_permission,
threshold_limit=80
)
@mock.patch('manila.share.drivers.dell_emc.plugins.isilon.'
'isilon_api.IsilonApi.create_session')
@@ -575,6 +580,30 @@ class IsilonApiTest(test.TestCase):
call_body = m.request_history[0].body
self.assertEqual(expected_request_json, json.loads(call_body))
@requests_mock.mock()
def test_quota_create_with_threshold(self, m):
quota_path = '/ifs/manila/test'
quota_size = 100
self.assertEqual(0, len(m.request_history))
m.post(self._mock_url + '/platform/1/quota/quotas', status_code=201)
self.isilon_api_threshold.quota_create(quota_path,
'directory',
quota_size)
advisory_size = round(
(quota_size * self.isilon_api_threshold.threshold_limit) / 100)
self.assertEqual(1, len(m.request_history))
expected_request_json = {
'path': quota_path,
'type': 'directory',
'include_snapshots': False,
'thresholds_include_overhead': False,
'enforced': True,
'thresholds': {'hard': quota_size,
'advisory': advisory_size},
}
call_body = m.request_history[0].body
self.assertEqual(expected_request_json, json.loads(call_body))
@requests_mock.mock()
def test_quota_create__path_does_not_exist(self, m):
quota_path = '/ifs/test2'
@@ -628,6 +657,22 @@ class IsilonApiTest(test.TestCase):
request_body = m.request_history[0].body
self.assertEqual(expected_request_body, json.loads(request_body))
@requests_mock.mock()
def test_quota_modify_with_threshold(self, m):
self.assertEqual(0, len(m.request_history))
quota_id = "ADEF1G"
new_size = 1024
advisory_size = round(
(new_size * self.isilon_api_threshold.threshold_limit) / 100)
m.put('{0}/platform/1/quota/quotas/{1}'.format(
self._mock_url, quota_id), status_code=204)
self.isilon_api_threshold.quota_modify_size(quota_id, new_size)
self.assertEqual(1, len(m.request_history))
expected_request_body = {'thresholds': {'hard': new_size,
'advisory': advisory_size}}
request_body = m.request_history[0].body
self.assertEqual(expected_request_body, json.loads(request_body))
@requests_mock.mock()
def test_quota_modify__given_id_does_not_exist(self, m):
quota_id = 'ADE2F'
@@ -886,6 +931,46 @@ class IsilonApiTest(test.TestCase):
self.assertRaises(exception.ShareBackendException,
self.isilon_api.get_space_stats)
def test_get_allocated_space_success(self):
self.isilon_api.send_get_request = mock.MagicMock()
self.isilon_api.send_get_request.return_value.status_code = 200
self.isilon_api.send_get_request.return_value.json.return_value = {
'quotas': [
{
'path': '/ifs/home',
'thresholds': {
'hard': None
}
},
{
'path': '/ifs/manila/CI-1d52ed66-a1ee-4b19-8f56-3706b',
'thresholds': {
'hard': 2147483648000
}
},
{
'path': '/ifs/manila/CI-0b622133-8b58-4a9f-ad1a-b8247',
'thresholds': {
'hard': 107374182400
}
},
{
'path': '/ifs/nilesh',
'thresholds': {
'hard': 10737418240
}
}
]
}
result = self.isilon_api.get_allocated_space()
self.assertEqual(result, 2110.0)
def test_get_allocated_space_failure(self):
self.isilon_api.send_get_request = mock.MagicMock()
self.isilon_api.send_get_request.return_value.status_code = 400
self.assertRaises(exception.ShareBackendException,
self.isilon_api.get_allocated_space)
def test_get_cluster_version_success(self):
self.isilon_api.send_get_request = mock.MagicMock()
self.isilon_api.send_get_request.return_value.status_code = 200

View File

@@ -0,0 +1,4 @@
---
features:
- |
Added support for thin provisioning on Dell PowerScale.