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:
Igor Malinovskiy 2015-05-18 17:23:04 +03:00
parent 36bc96be35
commit f67bcf81ae
13 changed files with 280 additions and 2 deletions

View File

@ -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",

View File

@ -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())

View File

@ -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 = (

View File

@ -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.")

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -15,6 +15,7 @@
"share:delete_snapshot": "",
"share:get_snapshot": "",
"share:get_all_snapshots": "",
"share:extend": "",
"share_network:create": "",
"share_network:index": "",

View File

@ -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):

View File

@ -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
)

View File

@ -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'})