From 0e4da58d9af1d252632e7399924066ccd57ec64b Mon Sep 17 00:00:00 2001 From: Shaun Edwards Date: Wed, 9 Dec 2015 15:38:30 -0800 Subject: [PATCH] EMC Isilon Driver Support For Extend Share Added support for "extend_share" in the Isilon driver. Change-Id: I307c75279f56ccd860ba30ca8f88ef414d6cee13 Implements: blueprint emc-isilon-driver-extend-share --- .../drivers/emc/plugins/isilon/isilon.py | 17 ++- .../drivers/emc/plugins/isilon/isilon_api.py | 63 +++++++- .../drivers/emc/plugins/isilon/test_isilon.py | 44 +++++- .../emc/plugins/isilon/test_isilon_api.py | 143 ++++++++++++++++++ 4 files changed, 257 insertions(+), 10 deletions(-) diff --git a/manila/share/drivers/emc/plugins/isilon/isilon.py b/manila/share/drivers/emc/plugins/isilon/isilon.py index 58dbeaaf0d..c75f3f2b4c 100644 --- a/manila/share/drivers/emc/plugins/isilon/isilon.py +++ b/manila/share/drivers/emc/plugins/isilon/isilon.py @@ -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.""" diff --git a/manila/share/drivers/emc/plugins/isilon/isilon_api.py b/manila/share/drivers/emc/plugins/isilon/isilon_api.py index 57fbd8c1be..6c742814f3 100644 --- a/manila/share/drivers/emc/plugins/isilon/isilon_api.py +++ b/manila/share/drivers/emc/plugins/isilon/isilon_api.py @@ -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 diff --git a/manila/tests/share/drivers/emc/plugins/isilon/test_isilon.py b/manila/tests/share/drivers/emc/plugins/isilon/test_isilon.py index 7e2de4d3b9..a4122aaa33 100644 --- a/manila/tests/share/drivers/emc/plugins/isilon/test_isilon.py +++ b/manila/tests/share/drivers/emc/plugins/isilon/test_isilon.py @@ -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) diff --git a/manila/tests/share/drivers/emc/plugins/isilon/test_isilon_api.py b/manila/tests/share/drivers/emc/plugins/isilon/test_isilon_api.py index 08e64f12fc..110f3a54ad 100644 --- a/manila/tests/share/drivers/emc/plugins/isilon/test_isilon_api.py +++ b/manila/tests/share/drivers/emc/plugins/isilon/test_isilon_api.py @@ -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))