Merge "EMC Isilon Driver Support For Extend Share"
This commit is contained in:
commit
6f4a3090b7
manila
share/drivers/emc/plugins/isilon
tests/share/drivers/emc/plugins/isilon
@ -20,6 +20,7 @@ import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
||||
from manila import exception
|
||||
@ -69,6 +70,12 @@ class IsilonStorageConnection(base.StorageConnection):
|
||||
{'proto': share['share_proto']})
|
||||
LOG.error(message)
|
||||
raise exception.InvalidShare(message=message)
|
||||
|
||||
# apply directory quota based on share size
|
||||
max_share_size = share['size'] * units.Gi
|
||||
self._isilon_api.quota_create(
|
||||
self._get_container_path(share), 'directory', max_share_size)
|
||||
|
||||
return location
|
||||
|
||||
def create_share_from_snapshot(self, context, share, snapshot,
|
||||
@ -161,13 +168,15 @@ class IsilonStorageConnection(base.StorageConnection):
|
||||
"""Is called to remove snapshot."""
|
||||
self._isilon_api.delete_snapshot(snapshot['name'])
|
||||
|
||||
def extend_share(self, share, new_size, share_server):
|
||||
"""Is called to extend share."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ensure_share(self, context, share, share_server):
|
||||
"""Invoked to ensure that share is exported."""
|
||||
|
||||
def extend_share(self, share, new_size, share_server=None):
|
||||
"""Extends a share."""
|
||||
new_quota_size = new_size * units.Gi
|
||||
self._isilon_api.quota_set(
|
||||
self._get_container_path(share), 'directory', new_quota_size)
|
||||
|
||||
def allow_access(self, context, share, access, share_server):
|
||||
"""Allow access to the share."""
|
||||
|
||||
|
@ -18,6 +18,9 @@ from oslo_serialization import jsonutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@ -208,9 +211,65 @@ class IsilonApi(object):
|
||||
.format(self.host_url, snapshot_name))
|
||||
response.raise_for_status()
|
||||
|
||||
def request(self, method, url, headers=None, data=None):
|
||||
def quota_create(self, path, quota_type, size):
|
||||
thresholds = {'hard': size}
|
||||
data = {
|
||||
'path': path,
|
||||
'type': quota_type,
|
||||
'include_snapshots': False,
|
||||
'thresholds_include_overhead': False,
|
||||
'enforced': True,
|
||||
'thresholds': thresholds,
|
||||
}
|
||||
response = self.request(
|
||||
'POST', '{0}/platform/1/quota/quotas'.format(self.host_url),
|
||||
data=data)
|
||||
response.raise_for_status()
|
||||
|
||||
def quota_get(self, path, quota_type):
|
||||
response = self.request(
|
||||
'GET',
|
||||
'{0}/platform/1/quota/quotas?path={1}'.format(self.host_url, path),
|
||||
)
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
elif response.status_code != 200:
|
||||
response.raise_for_status()
|
||||
|
||||
json = response.json()
|
||||
len_returned_quotas = len(json['quotas'])
|
||||
if len_returned_quotas == 0:
|
||||
return None
|
||||
elif len_returned_quotas == 1:
|
||||
return json['quotas'][0]
|
||||
else:
|
||||
message = (_('Greater than one quota returned when querying '
|
||||
'quotas associated with share path: %(path)s .') %
|
||||
{'path': path})
|
||||
raise exception.ShareBackendException(msg=message)
|
||||
|
||||
def quota_modify_size(self, quota_id, new_size):
|
||||
data = {'thresholds': {'hard': new_size}}
|
||||
response = self.request(
|
||||
'PUT',
|
||||
'{0}/platform/1/quota/quotas/{1}'.format(self.host_url, quota_id),
|
||||
data=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def quota_set(self, path, quota_type, size):
|
||||
"""Sets a quota of the given type and size on the given path."""
|
||||
quota_json = self.quota_get(path, quota_type)
|
||||
if quota_json is None:
|
||||
self.quota_create(path, quota_type, size)
|
||||
else:
|
||||
# quota already exists, modify it's size
|
||||
quota_id = quota_json['id']
|
||||
self.quota_modify_size(quota_id, size)
|
||||
|
||||
def request(self, method, url, headers=None, data=None, params=None):
|
||||
if data is not None:
|
||||
data = jsonutils.dumps(data)
|
||||
r = self.session.request(method, url, headers=headers, data=data,
|
||||
verify=self.verify_ssl_cert)
|
||||
verify=self.verify_ssl_cert, params=params)
|
||||
return r
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
import mock
|
||||
from oslo_log import log
|
||||
from oslo_utils import units
|
||||
|
||||
from manila import exception
|
||||
from manila.share.drivers.emc.plugins.isilon import isilon
|
||||
@ -312,7 +313,7 @@ class IsilonTest(test.TestCase):
|
||||
self.assertFalse(self._mock_isilon_api.create_nfs_export.called)
|
||||
|
||||
# create the share
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'NFS'}
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'NFS', "size": 8}
|
||||
location = self.storage_connection.create_share(self.mock_context,
|
||||
share, None)
|
||||
|
||||
@ -322,12 +323,16 @@ class IsilonTest(test.TestCase):
|
||||
self._mock_isilon_api.create_directory.assert_called_with(share_path)
|
||||
self._mock_isilon_api.create_nfs_export.assert_called_with(share_path)
|
||||
|
||||
# verify directory quota call made
|
||||
self._mock_isilon_api.quota_create.assert_called_with(
|
||||
share_path, 'directory', 8 * units.Gi)
|
||||
|
||||
def test_create_share_cifs(self):
|
||||
self.assertFalse(self._mock_isilon_api.create_directory.called)
|
||||
self.assertFalse(self._mock_isilon_api.create_smb_share.called)
|
||||
|
||||
# create the share
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'CIFS'}
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'CIFS', "size": 8}
|
||||
location = self.storage_connection.create_share(self.mock_context,
|
||||
share, None)
|
||||
|
||||
@ -339,6 +344,10 @@ class IsilonTest(test.TestCase):
|
||||
self._mock_isilon_api.create_smb_share.assert_called_once_with(
|
||||
self.SHARE_NAME, self.SHARE_DIR)
|
||||
|
||||
# verify directory quota call made
|
||||
self._mock_isilon_api.quota_create.assert_called_with(
|
||||
self.SHARE_DIR, 'directory', 8 * units.Gi)
|
||||
|
||||
def test_create_share_invalid_share_protocol(self):
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'FOO_PROTOCOL'}
|
||||
|
||||
@ -378,7 +387,7 @@ class IsilonTest(test.TestCase):
|
||||
|
||||
# execute method under test
|
||||
snapshot = {'name': snapshot_name, 'share_name': snapshot_path}
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'NFS'}
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'NFS', 'size': 5}
|
||||
location = self.storage_connection.create_share_from_snapshot(
|
||||
self.mock_context, share, snapshot, None)
|
||||
|
||||
@ -393,6 +402,10 @@ class IsilonTest(test.TestCase):
|
||||
self.ISILON_ADDR, self.SHARE_DIR)
|
||||
self.assertEqual(expected_location, location)
|
||||
|
||||
# verify directory quota call made
|
||||
self._mock_isilon_api.quota_create.assert_called_with(
|
||||
self.SHARE_DIR, 'directory', 5 * units.Gi)
|
||||
|
||||
def test_create_share_from_snapshot_cifs(self):
|
||||
# assertions
|
||||
self.assertFalse(self._mock_isilon_api.create_smb_share.called)
|
||||
@ -404,7 +417,7 @@ class IsilonTest(test.TestCase):
|
||||
|
||||
# execute method under test
|
||||
snapshot = {'name': snapshot_name, 'share_name': snapshot_path}
|
||||
share = {"name": new_share_name, "share_proto": 'CIFS'}
|
||||
share = {"name": new_share_name, "share_proto": 'CIFS', "size": 2}
|
||||
location = self.storage_connection.create_share_from_snapshot(
|
||||
self.mock_context, share, snapshot, None)
|
||||
|
||||
@ -417,6 +430,11 @@ class IsilonTest(test.TestCase):
|
||||
new_share_name)
|
||||
self.assertEqual(expected_location, location)
|
||||
|
||||
# verify directory quota call made
|
||||
expected_share_path = '{0}/{1}'.format(self.ROOT_DIR, new_share_name)
|
||||
self._mock_isilon_api.quota_create.assert_called_with(
|
||||
expected_share_path, 'directory', 2 * units.Gi)
|
||||
|
||||
def test_delete_share_nfs(self):
|
||||
share = {"name": self.SHARE_NAME, "share_proto": 'NFS'}
|
||||
fake_share_num = 42
|
||||
@ -553,3 +571,21 @@ class IsilonTest(test.TestCase):
|
||||
num = self.storage_connection.get_network_allocations_number()
|
||||
|
||||
self.assertEqual(0, num)
|
||||
|
||||
def test_extend_share(self):
|
||||
quota_id = 'abcdef'
|
||||
new_share_size = 8
|
||||
share = {
|
||||
"name": self.SHARE_NAME,
|
||||
"share_proto": 'NFS',
|
||||
"size": new_share_size
|
||||
}
|
||||
self._mock_isilon_api.quota_get.return_value = {'id': quota_id}
|
||||
self.assertFalse(self._mock_isilon_api.quota_set.called)
|
||||
|
||||
self.storage_connection.extend_share(share, new_share_size)
|
||||
|
||||
share_path = '{0}/{1}'.format(self.ROOT_DIR, self.SHARE_NAME)
|
||||
expected_quota_size = new_share_size * units.Gi
|
||||
self._mock_isilon_api.quota_set.assert_called_once_with(
|
||||
share_path, 'directory', expected_quota_size)
|
||||
|
@ -469,6 +469,149 @@ class IsilonApiTest(test.TestCase):
|
||||
self.assertRaises(requests.exceptions.HTTPError,
|
||||
self.isilon_api.delete_snapshot, "my_snapshot")
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_create(self, m):
|
||||
quota_path = '/ifs/manila/test'
|
||||
quota_size = 256
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
m.post(self._mock_url + '/platform/1/quota/quotas', status_code=201)
|
||||
|
||||
self.isilon_api.quota_create(quota_path, 'directory', quota_size)
|
||||
|
||||
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},
|
||||
}
|
||||
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'
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
m.post(self._mock_url + '/platform/1/quota/quotas', status_code=400)
|
||||
|
||||
self.assertRaises(
|
||||
requests.exceptions.HTTPError,
|
||||
self.isilon_api.quota_create,
|
||||
quota_path, 'directory', 2
|
||||
)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_get(self, m):
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
response_json = {'quotas': [{}]}
|
||||
m.get(self._mock_url + '/platform/1/quota/quotas', json=response_json,
|
||||
status_code=200)
|
||||
quota_path = "/ifs/manila/test"
|
||||
quota_type = "directory"
|
||||
|
||||
self.isilon_api.quota_get(quota_path, quota_type)
|
||||
|
||||
self.assertEqual(1, len(m.request_history))
|
||||
request_query_string = m.request_history[0].qs
|
||||
expected_query_string = {'path': [quota_path]}
|
||||
self.assertEqual(expected_query_string, request_query_string)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_get__path_does_not_exist(self, m):
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
m.get(self._mock_url + '/platform/1/quota/quotas', status_code=404)
|
||||
|
||||
response = self.isilon_api.quota_get(
|
||||
'/ifs/does_not_exist', 'directory')
|
||||
|
||||
self.assertIsNone(response)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_modify(self, m):
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
quota_id = "ADEF1G"
|
||||
new_size = 1024
|
||||
m.put('{0}/platform/1/quota/quotas/{1}'.format(
|
||||
self._mock_url, quota_id), status_code=204)
|
||||
|
||||
self.isilon_api.quota_modify_size(quota_id, new_size)
|
||||
|
||||
self.assertEqual(1, len(m.request_history))
|
||||
expected_request_body = {'thresholds': {'hard': new_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'
|
||||
m.put('{0}/platform/1/quota/quotas/{1}'.format(
|
||||
self._mock_url, quota_id), status_code=404)
|
||||
|
||||
self.assertRaises(
|
||||
requests.exceptions.HTTPError,
|
||||
self.isilon_api.quota_modify_size,
|
||||
quota_id, 1024
|
||||
)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_set__quota_already_exists(self, m):
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
quota_path = '/ifs/manila/test'
|
||||
quota_type = 'directory'
|
||||
quota_size = 256
|
||||
quota_id = 'AFE2C'
|
||||
m.get('{0}/platform/1/quota/quotas'.format(
|
||||
self._mock_url), json={'quotas': [{'id': quota_id}]},
|
||||
status_code=200)
|
||||
m.put(
|
||||
'{0}/platform/1/quota/quotas/{1}'.format(self._mock_url, quota_id),
|
||||
status_code=204
|
||||
)
|
||||
|
||||
self.isilon_api.quota_set(quota_path, quota_type, quota_size)
|
||||
|
||||
expected_quota_modify_json = {'thresholds': {'hard': quota_size}}
|
||||
quota_put_json = json.loads(m.request_history[1].body)
|
||||
self.assertEqual(expected_quota_modify_json, quota_put_json)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_set__quota_does_not_already_exist(self, m):
|
||||
self.assertEqual(0, len(m.request_history))
|
||||
m.get('{0}/platform/1/quota/quotas'.format(
|
||||
self._mock_url), status_code=404)
|
||||
m.post('{0}/platform/1/quota/quotas'.format(self._mock_url),
|
||||
status_code=201)
|
||||
quota_path = '/ifs/manila/test'
|
||||
quota_type = 'directory'
|
||||
quota_size = 256
|
||||
|
||||
self.isilon_api.quota_set(quota_path, quota_type, quota_size)
|
||||
|
||||
# verify a call is made to create a quota
|
||||
expected_create_json = {
|
||||
six.text_type('path'): quota_path,
|
||||
six.text_type('type'): 'directory',
|
||||
six.text_type('include_snapshots'): False,
|
||||
six.text_type('thresholds_include_overhead'): False,
|
||||
six.text_type('enforced'): True,
|
||||
six.text_type('thresholds'): {six.text_type('hard'): quota_size},
|
||||
}
|
||||
create_request_json = json.loads(m.request_history[1].body)
|
||||
self.assertEqual(expected_create_json, create_request_json)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_quota_set__path_does_not_already_exist(self, m):
|
||||
m.get(self._mock_url + '/platform/1/quota/quotas', status_code=400)
|
||||
|
||||
e = self.assertRaises(
|
||||
requests.exceptions.HTTPError,
|
||||
self.isilon_api.quota_set,
|
||||
'/ifs/does_not_exist', 'directory', 2048
|
||||
)
|
||||
self.assertEqual(400, e.response.status_code)
|
||||
|
||||
def _add_create_directory_response(self, m, path, is_recursive):
|
||||
url = '{0}/namespace{1}?recursive={2}'.format(
|
||||
self._mock_url, path, six.text_type(is_recursive))
|
||||
|
Loading…
x
Reference in New Issue
Block a user