Add share extend API
Implement API for extending shares similar to Cinder: "manila extend <share-id> <new-size>" - Implement tenant-facing API for extending shares - Add appropriate unit tests Partially implements bp share-extend-api Change-Id: Ic63ecb1c2881ac9c8b59010efe3a37413f18f28d
This commit is contained in:
parent
36bc96be35
commit
f67bcf81ae
@ -20,6 +20,7 @@
|
||||
"share:access_get_all": "rule:default",
|
||||
"share:allow_access": "rule:default",
|
||||
"share:deny_access": "rule:default",
|
||||
"share:extend": "rule:default",
|
||||
"share:get_share_metadata": "rule:default",
|
||||
"share:delete_share_metadata": "rule:default",
|
||||
"share:update_share_metadata": "rule:default",
|
||||
|
@ -135,6 +135,30 @@ class ShareActionsController(wsgi.Controller):
|
||||
access_list = self.share_api.access_get_all(context, share)
|
||||
return {'access_list': access_list}
|
||||
|
||||
@wsgi.action('os-extend')
|
||||
def _extend(self, req, id, body):
|
||||
"""Extend size of share."""
|
||||
context = req.environ['manila.context']
|
||||
try:
|
||||
share = self.share_api.get(context, id)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(e))
|
||||
|
||||
try:
|
||||
size = int(body['os-extend']['new_size'])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
msg = _("New share size must be specified as an integer.")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
self.share_api.extend(context, share, size)
|
||||
except (exception.InvalidInput, exception.InvalidShare) as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
except exception.ShareSizeExceedsAvailableQuota as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=six.text_type(e))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
# def create_resource():
|
||||
# return wsgi.Resource(ShareActionsController())
|
||||
|
@ -29,11 +29,14 @@ STATUS_MANAGE_ERROR = 'MANAGE_ERROR'
|
||||
STATUS_UNMANAGING = 'UNMANAGE_STARTING'
|
||||
STATUS_UNMANAGE_ERROR = 'UNMANAGE_ERROR'
|
||||
STATUS_UNMANAGED = 'UNMANAGED'
|
||||
STATUS_EXTENDING = 'EXTENDING'
|
||||
STATUS_EXTENDING_ERROR = 'EXTENDING_ERROR'
|
||||
|
||||
TRANSITIONAL_STATUSES = (
|
||||
STATUS_CREATING, STATUS_DELETING,
|
||||
STATUS_ACTIVATING, STATUS_DEACTIVATING,
|
||||
STATUS_MANAGING, STATUS_UNMANAGING,
|
||||
STATUS_EXTENDING,
|
||||
)
|
||||
|
||||
SUPPORTED_SHARE_PROTOCOLS = (
|
||||
|
@ -482,6 +482,11 @@ class ManageExistingShareTypeMismatch(ManilaException):
|
||||
"%(reason)s")
|
||||
|
||||
|
||||
class ShareExtendingError(ManilaException):
|
||||
message = _("Share %(share_id)s could not be extended due to error "
|
||||
"in the driver: %(reason)s")
|
||||
|
||||
|
||||
class InstanceNotFound(NotFound):
|
||||
message = _("Instance %(instance_id)s could not be found.")
|
||||
|
||||
|
@ -32,6 +32,7 @@ from manila.db import base
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.i18n import _LE
|
||||
from manila.i18n import _LI
|
||||
from manila.i18n import _LW
|
||||
from manila import policy
|
||||
from manila import quota
|
||||
@ -674,3 +675,55 @@ class API(base.Base):
|
||||
|
||||
def get_share_network(self, context, share_net_id):
|
||||
return self.db.share_network_get(context, share_net_id)
|
||||
|
||||
def extend(self, context, share, new_size):
|
||||
policy.check_policy(context, 'share', 'extend')
|
||||
|
||||
status = six.text_type(share['status']).upper()
|
||||
|
||||
if status != constants.STATUS_AVAILABLE:
|
||||
msg_params = {
|
||||
'valid_status': constants.STATUS_AVAILABLE,
|
||||
'share_id': share['id'],
|
||||
'status': status,
|
||||
}
|
||||
msg = _("Share %(share_id)s status must be '%(valid_status)s' "
|
||||
"to extend, but current status is: "
|
||||
"%(status)s.") % msg_params
|
||||
raise exception.InvalidShare(reason=msg)
|
||||
|
||||
size_increase = int(new_size) - share['size']
|
||||
if size_increase <= 0:
|
||||
msg = (_("New size for extend must be greater "
|
||||
"than current size. (current: %(size)s, "
|
||||
"extended: %(new_size)s).") % {'new_size': new_size,
|
||||
'size': share['size']})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
try:
|
||||
reservations = QUOTAS.reserve(context,
|
||||
project_id=share['project_id'],
|
||||
gigabytes=size_increase)
|
||||
except exception.OverQuota as exc:
|
||||
usages = exc.kwargs['usages']
|
||||
quotas = exc.kwargs['quotas']
|
||||
|
||||
def _consumed(name):
|
||||
return usages[name]['reserved'] + usages[name]['in_use']
|
||||
|
||||
msg = _LE("Quota exceeded for %(s_pid)s, tried to extend share "
|
||||
"by %(s_size)sG, (%(d_consumed)dG of %(d_quota)dG "
|
||||
"already consumed).")
|
||||
LOG.error(msg, {'s_pid': context.project_id,
|
||||
's_size': size_increase,
|
||||
'd_consumed': _consumed('gigabytes'),
|
||||
'd_quota': quotas['gigabytes']})
|
||||
raise exception.ShareSizeExceedsAvailableQuota(
|
||||
requested=size_increase,
|
||||
consumed=_consumed('gigabytes'),
|
||||
quota=quotas['gigabytes'])
|
||||
|
||||
self.update(context, share, {'status': constants.STATUS_EXTENDING})
|
||||
self.share_rpcapi.extend_share(context, share, new_size, reservations)
|
||||
LOG.info(_LI("Extend share request issued successfully."),
|
||||
resource=share)
|
||||
|
@ -375,6 +375,15 @@ class ShareDriver(object):
|
||||
UnmanageInvalidShare exception, specifying a reason for the failure.
|
||||
"""
|
||||
|
||||
def extend_share(self, share, new_size, share_server=None):
|
||||
"""Extends size of existing share.
|
||||
|
||||
:param share: Share model
|
||||
:param new_size: New size of share (new_size > share['size'])
|
||||
:param share_server: Optional -- Share server model
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def teardown_server(self, *args, **kwargs):
|
||||
if self.driver_handles_share_servers:
|
||||
return self._teardown_server(*args, **kwargs)
|
||||
|
@ -92,7 +92,7 @@ QUOTAS = quota.QUOTAS
|
||||
class ShareManager(manager.SchedulerDependentManager):
|
||||
"""Manages NAS storages."""
|
||||
|
||||
RPC_API_VERSION = '1.1'
|
||||
RPC_API_VERSION = '1.2'
|
||||
|
||||
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
|
||||
"""Load the driver from args, or from flags."""
|
||||
@ -833,3 +833,37 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
raise exception.InvalidParameterValue(
|
||||
"Option unused_share_server_cleanup_interval should be "
|
||||
"between 10 minutes and 1 hour.")
|
||||
|
||||
def extend_share(self, context, share_id, new_size, reservations):
|
||||
context = context.elevated()
|
||||
share = self.db.share_get(context, share_id)
|
||||
share_server = self._get_share_server(context, share)
|
||||
project_id = share['project_id']
|
||||
|
||||
try:
|
||||
self.driver.extend_share(
|
||||
share, new_size, share_server=share_server)
|
||||
except Exception as e:
|
||||
LOG.exception(_LE("Extend share failed."), resource=share)
|
||||
|
||||
try:
|
||||
self.db.share_update(
|
||||
context, share['id'],
|
||||
{'status': constants.STATUS_EXTENDING_ERROR}
|
||||
)
|
||||
raise exception.ShareExtendingError(
|
||||
reason=six.text_type(e), share_id=share_id)
|
||||
finally:
|
||||
QUOTAS.rollback(context, reservations, project_id=project_id)
|
||||
|
||||
QUOTAS.commit(context, reservations, project_id=project_id)
|
||||
|
||||
share_update = {
|
||||
'size': int(new_size),
|
||||
# NOTE(u_glide): translation to lower case should be removed in
|
||||
# a row with usage of upper case of share statuses in all places
|
||||
'status': constants.STATUS_AVAILABLE.lower()
|
||||
}
|
||||
share = self.db.share_update(context, share['id'], share_update)
|
||||
|
||||
LOG.info(_LI("Extend share completed successfully."), resource=share)
|
||||
|
@ -33,6 +33,7 @@ class ShareAPI(object):
|
||||
|
||||
1.0 - Initial version.
|
||||
1.1 - Add manage_share() and unmanage_share() methods
|
||||
1.2 - Add extend_share() method
|
||||
'''
|
||||
|
||||
BASE_RPC_API_VERSION = '1.0'
|
||||
@ -41,7 +42,7 @@ class ShareAPI(object):
|
||||
super(ShareAPI, self).__init__()
|
||||
target = messaging.Target(topic=CONF.share_topic,
|
||||
version=self.BASE_RPC_API_VERSION)
|
||||
self.client = rpc.get_client(target, version_cap='1.1')
|
||||
self.client = rpc.get_client(target, version_cap='1.2')
|
||||
|
||||
def create_share(self, ctxt, share, host,
|
||||
request_spec, filter_properties,
|
||||
@ -109,3 +110,9 @@ class ShareAPI(object):
|
||||
def publish_service_capabilities(self, ctxt):
|
||||
cctxt = self.client.prepare(fanout=True, version='1.0')
|
||||
cctxt.cast(ctxt, 'publish_service_capabilities')
|
||||
|
||||
def extend_share(self, ctxt, share, new_size, reservations):
|
||||
host = utils.extract_host(share['host'])
|
||||
cctxt = self.client.prepare(server=host, version='1.2')
|
||||
cctxt.cast(ctxt, 'extend_share', share_id=share['id'],
|
||||
new_size=new_size, reservations=reservations)
|
||||
|
@ -18,6 +18,7 @@ from oslo_config import cfg
|
||||
import webob
|
||||
|
||||
from manila.api.contrib import share_actions
|
||||
from manila import exception
|
||||
from manila.share import api as share_api
|
||||
from manila import test
|
||||
from manila.tests.api.contrib import stubs
|
||||
@ -140,3 +141,46 @@ class ShareActionsTest(test.TestCase):
|
||||
res_dict = self.controller._access_list(req, id, body)
|
||||
expected = _fake_access_get_all()
|
||||
self.assertEqual(res_dict['access_list'], expected)
|
||||
|
||||
def test_extend(self):
|
||||
id = 'fake_share_id'
|
||||
share = stubs.stub_share_get(None, None, id)
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(share_api.API, "extend")
|
||||
|
||||
size = '123'
|
||||
body = {"os-extend": {'new_size': size}}
|
||||
req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id)
|
||||
|
||||
actual_response = self.controller._extend(req, id, body)
|
||||
|
||||
share_api.API.get.assert_called_once_with(mock.ANY, id)
|
||||
share_api.API.extend.assert_called_once_with(
|
||||
mock.ANY, share, int(size))
|
||||
self.assertEqual(202, actual_response.status_int)
|
||||
|
||||
@ddt.data({"os-extend": ""},
|
||||
{"os-extend": {"new_size": "foo"}},
|
||||
{"os-extend": {"new_size": {'foo': 'bar'}}})
|
||||
def test_extend_invalid_body(self, body):
|
||||
id = 'fake_share_id'
|
||||
req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id)
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._extend, req, id, body)
|
||||
|
||||
@ddt.data({'source': exception.InvalidInput,
|
||||
'target': webob.exc.HTTPBadRequest},
|
||||
{'source': exception.InvalidShare,
|
||||
'target': webob.exc.HTTPBadRequest},
|
||||
{'source': exception.ShareSizeExceedsAvailableQuota,
|
||||
'target': webob.exc.HTTPForbidden})
|
||||
@ddt.unpack
|
||||
def test_extend_exception(self, source, target):
|
||||
id = 'fake_share_id'
|
||||
req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id)
|
||||
body = {"os-extend": {'new_size': '123'}}
|
||||
self.mock_object(share_api.API, "extend",
|
||||
mock.Mock(side_effect=source('fake')))
|
||||
|
||||
self.assertRaises(target, self.controller._extend, req, id, body)
|
||||
|
@ -15,6 +15,7 @@
|
||||
"share:delete_snapshot": "",
|
||||
"share:get_snapshot": "",
|
||||
"share:get_all_snapshots": "",
|
||||
"share:extend": "",
|
||||
|
||||
"share_network:create": "",
|
||||
"share_network:index": "",
|
||||
|
@ -1360,6 +1360,46 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertEqual(should_be,
|
||||
db_driver.share_metadata_get(self.context, share_id))
|
||||
|
||||
def test_extend_invalid_status(self):
|
||||
invalid_status = 'fake'
|
||||
share = fake_share('fake', status=invalid_status)
|
||||
new_size = 123
|
||||
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self.api.extend, self.context, share, new_size)
|
||||
|
||||
def test_extend_invalid_size(self):
|
||||
share = fake_share('fake', status=constants.STATUS_AVAILABLE, size=200)
|
||||
new_size = 123
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.extend, self.context, share, new_size)
|
||||
|
||||
def test_extend_quota_error(self):
|
||||
share = fake_share('fake', status=constants.STATUS_AVAILABLE, size=100)
|
||||
new_size = 123
|
||||
usages = {'gigabytes': {'reserved': 'fake', 'in_use': 'fake'}}
|
||||
quotas = {'gigabytes': 'fake'}
|
||||
exc = exception.OverQuota(usages=usages, quotas=quotas)
|
||||
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock(side_effect=exc))
|
||||
|
||||
self.assertRaises(exception.ShareSizeExceedsAvailableQuota,
|
||||
self.api.extend, self.context, share, new_size)
|
||||
|
||||
def test_extend_valid(self):
|
||||
share = fake_share('fake', status=constants.STATUS_AVAILABLE, size=100)
|
||||
new_size = 123
|
||||
self.mock_object(self.api, 'update')
|
||||
self.mock_object(self.api.share_rpcapi, 'extend_share')
|
||||
|
||||
self.api.extend(self.context, share, new_size)
|
||||
|
||||
self.api.update.assert_called_once_with(
|
||||
self.context, share, {'status': constants.STATUS_EXTENDING})
|
||||
self.api.share_rpcapi.extend_share.assert_called_once_with(
|
||||
self.context, share, new_size, mock.ANY
|
||||
)
|
||||
|
||||
|
||||
class OtherTenantsShareActionsTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -1514,3 +1514,52 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.context,
|
||||
'server1')
|
||||
timeutils.utcnow.assert_called_once_with()
|
||||
|
||||
def test_extend_share_invalid(self):
|
||||
share = self._create_share()
|
||||
share_id = share['id']
|
||||
|
||||
self.mock_object(self.share_manager, 'driver')
|
||||
self.mock_object(self.share_manager.db, 'share_update')
|
||||
self.mock_object(quota.QUOTAS, 'rollback')
|
||||
self.mock_object(self.share_manager.driver, 'extend_share',
|
||||
mock.Mock(side_effect=Exception('fake')))
|
||||
|
||||
self.assertRaises(
|
||||
exception.ShareExtendingError,
|
||||
self.share_manager.extend_share, self.context, share_id, 123, {})
|
||||
|
||||
def test_extend_share(self):
|
||||
share = self._create_share()
|
||||
share_id = share['id']
|
||||
new_size = 123
|
||||
shr_update = {
|
||||
'size': int(new_size),
|
||||
'status': constants.STATUS_AVAILABLE.lower()
|
||||
}
|
||||
reservations = {}
|
||||
fake_share_server = 'fake'
|
||||
|
||||
manager = self.share_manager
|
||||
self.mock_object(manager, 'driver')
|
||||
self.mock_object(manager.db, 'share_get',
|
||||
mock.Mock(return_value=share))
|
||||
self.mock_object(manager.db, 'share_update',
|
||||
mock.Mock(return_value=share))
|
||||
self.mock_object(quota.QUOTAS, 'commit')
|
||||
self.mock_object(manager.driver, 'extend_share')
|
||||
self.mock_object(manager, '_get_share_server',
|
||||
mock.Mock(return_value=fake_share_server))
|
||||
|
||||
self.share_manager.extend_share(self.context, share_id,
|
||||
new_size, reservations)
|
||||
|
||||
self.assertTrue(manager._get_share_server.called)
|
||||
manager.driver.extend_share.assert_called_once_with(
|
||||
share, new_size, share_server=fake_share_server
|
||||
)
|
||||
quota.QUOTAS.commit.assert_called_once_with(
|
||||
mock.ANY, reservations, project_id=share['project_id'])
|
||||
manager.db.share_update.assert_called_once_with(
|
||||
mock.ANY, share_id, shr_update
|
||||
)
|
@ -161,3 +161,11 @@ class ShareRpcAPITestCase(test.TestCase):
|
||||
self._test_share_api('delete_share_server',
|
||||
rpc_method='cast',
|
||||
share_server=self.fake_share_server)
|
||||
|
||||
def test_extend_share(self):
|
||||
self._test_share_api('extend_share',
|
||||
rpc_method='cast',
|
||||
version='1.2',
|
||||
share=self.fake_share,
|
||||
new_size=123,
|
||||
reservations={'fake': 'fake'})
|
||||
|
Loading…
Reference in New Issue
Block a user