Add quota support for checkpoints resource
Change-Id: I68bbc48ce6fb1638455d2bf67027d497f34fea64 Implements: blueprint support-quotas-in-karbor
This commit is contained in:
parent
39bdc2a891
commit
829948b328
|
@ -29,6 +29,7 @@ from karbor.i18n import _
|
||||||
|
|
||||||
from karbor import objects
|
from karbor import objects
|
||||||
from karbor.policies import providers as provider_policy
|
from karbor.policies import providers as provider_policy
|
||||||
|
from karbor import quota
|
||||||
from karbor.services.protection import api as protection_api
|
from karbor.services.protection import api as protection_api
|
||||||
from karbor import utils
|
from karbor import utils
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ query_provider_filters_opts = [
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
QUOTAS = quota.QUOTAS
|
||||||
|
|
||||||
query_checkpoint_filters_opts = [
|
query_checkpoint_filters_opts = [
|
||||||
cfg.ListOpt(
|
cfg.ListOpt(
|
||||||
|
@ -391,10 +393,23 @@ class ProvidersController(wsgi.Controller):
|
||||||
},
|
},
|
||||||
"extra_info": checkpoint_extra_info
|
"extra_info": checkpoint_extra_info
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checkpoint_id = self.protection_api.protect(context, plan,
|
reserve_opts = {'checkpoints': 1}
|
||||||
checkpoint_properties)
|
reservations = QUOTAS.reserve(context, **reserve_opts)
|
||||||
|
except exception.OverQuota as e:
|
||||||
|
quota.process_reserve_over_quota(
|
||||||
|
context, e,
|
||||||
|
resource='checkpoints')
|
||||||
|
else:
|
||||||
|
checkpoint_id = None
|
||||||
|
try:
|
||||||
|
checkpoint_id = self.protection_api.protect(
|
||||||
|
context, plan, checkpoint_properties)
|
||||||
|
QUOTAS.commit(context, reservations)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
if not checkpoint_id:
|
||||||
|
QUOTAS.rollback(context, reservations)
|
||||||
msg = _("Create checkpoint failed: %s") % error
|
msg = _("Create checkpoint failed: %s") % error
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
@ -455,24 +470,33 @@ class ProvidersController(wsgi.Controller):
|
||||||
def checkpoints_delete(self, req, provider_id, checkpoint_id):
|
def checkpoints_delete(self, req, provider_id, checkpoint_id):
|
||||||
"""Delete a checkpoint."""
|
"""Delete a checkpoint."""
|
||||||
context = req.environ['karbor.context']
|
context = req.environ['karbor.context']
|
||||||
|
context.can(provider_policy.CHECKPOINT_DELETE_POLICY)
|
||||||
|
|
||||||
LOG.info("Delete checkpoint with id: %s.", checkpoint_id)
|
LOG.info("Delete checkpoint with id: %s.", checkpoint_id)
|
||||||
LOG.info("provider_id: %s.", provider_id)
|
LOG.info("provider_id: %s.", provider_id)
|
||||||
|
try:
|
||||||
|
checkpoint = self._checkpoint_get(context, provider_id,
|
||||||
|
checkpoint_id)
|
||||||
|
except exception.CheckpointNotFound as error:
|
||||||
|
raise exc.HTTPNotFound(explanation=error.msg)
|
||||||
|
except exception.AccessCheckpointNotAllowed as error:
|
||||||
|
raise exc.HTTPForbidden(explanation=error.msg)
|
||||||
|
project_id = checkpoint.get('project_id')
|
||||||
|
|
||||||
if not uuidutils.is_uuid_like(provider_id):
|
|
||||||
msg = _("Invalid provider id provided.")
|
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
if not uuidutils.is_uuid_like(checkpoint_id):
|
|
||||||
msg = _("Invalid checkpoint id provided.")
|
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
context.can(provider_policy.CHECKPOINT_DELETE_POLICY)
|
|
||||||
try:
|
try:
|
||||||
self.protection_api.delete(context, provider_id, checkpoint_id)
|
self.protection_api.delete(context, provider_id, checkpoint_id)
|
||||||
except exception.DeleteCheckpointNotAllowed as error:
|
except exception.DeleteCheckpointNotAllowed as error:
|
||||||
raise exc.HTTPForbidden(explantion=error.msg)
|
raise exc.HTTPForbidden(explantion=error.msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reserve_opts = {'checkpoints': -1}
|
||||||
|
reservations = QUOTAS.reserve(
|
||||||
|
context, project_id=project_id, **reserve_opts)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Failed to update usages after deleting checkpoint.")
|
||||||
|
else:
|
||||||
|
QUOTAS.commit(context, reservations, project_id=project_id)
|
||||||
|
|
||||||
LOG.info("Delete checkpoint request issued successfully.")
|
LOG.info("Delete checkpoint request issued successfully.")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ class QuotaClassesViewBuilder(common.ViewBuilder):
|
||||||
"""Detailed view of a single quota class."""
|
"""Detailed view of a single quota class."""
|
||||||
keys = (
|
keys = (
|
||||||
'plans',
|
'plans',
|
||||||
|
'checkpoints',
|
||||||
)
|
)
|
||||||
view = {key: quota.get(key) for key in keys}
|
view = {key: quota.get(key) for key in keys}
|
||||||
if quota_class:
|
if quota_class:
|
||||||
|
|
|
@ -46,6 +46,7 @@ class QuotasViewBuilder(common.ViewBuilder):
|
||||||
"""Detailed view of a single quota."""
|
"""Detailed view of a single quota."""
|
||||||
keys = (
|
keys = (
|
||||||
'plans',
|
'plans',
|
||||||
|
'checkpoints',
|
||||||
)
|
)
|
||||||
view = {key: quota.get(key) for key in keys}
|
view = {key: quota.get(key) for key in keys}
|
||||||
if project_id:
|
if project_id:
|
||||||
|
|
|
@ -447,6 +447,10 @@ class PlanLimitExceeded(QuotaError):
|
||||||
message = _("Maximum number of plans allowed (%(allowed)d) exceeded")
|
message = _("Maximum number of plans allowed (%(allowed)d) exceeded")
|
||||||
|
|
||||||
|
|
||||||
|
class CheckpointLimitExceeded(QuotaError):
|
||||||
|
message = _("Maximum number of checkpoints allowed (%(allowed)d) exceeded")
|
||||||
|
|
||||||
|
|
||||||
class UnexpectedOverQuota(QuotaError):
|
class UnexpectedOverQuota(QuotaError):
|
||||||
message = _("Unexpected over quota on %(name)s.")
|
message = _("Unexpected over quota on %(name)s.")
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,9 @@ quota_opts = [
|
||||||
cfg.IntOpt('quota_plans',
|
cfg.IntOpt('quota_plans',
|
||||||
default=50,
|
default=50,
|
||||||
help='The number of volume backups allowed per project'),
|
help='The number of volume backups allowed per project'),
|
||||||
|
cfg.IntOpt('quota_checkpoints',
|
||||||
|
default=-1,
|
||||||
|
help='The number of checkpoints allowed per project'),
|
||||||
cfg.IntOpt('reservation_expire',
|
cfg.IntOpt('reservation_expire',
|
||||||
default=86400,
|
default=86400,
|
||||||
help='number of seconds until a reservation expires'),
|
help='number of seconds until a reservation expires'),
|
||||||
|
@ -766,13 +769,18 @@ QUOTAS = QuotaEngine()
|
||||||
resources = [
|
resources = [
|
||||||
ReservableResource('plans', None,
|
ReservableResource('plans', None,
|
||||||
'quota_plans'),
|
'quota_plans'),
|
||||||
|
ReservableResource('checkpoints', None,
|
||||||
|
'quota_checkpoints'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
QUOTAS.register_resources(resources)
|
QUOTAS.register_resources(resources)
|
||||||
|
|
||||||
|
|
||||||
OVER_QUOTA_RESOURCE_EXCEPTIONS = {'plans': exception.PlanLimitExceeded}
|
OVER_QUOTA_RESOURCE_EXCEPTIONS = {
|
||||||
|
'plans': exception.PlanLimitExceeded,
|
||||||
|
'checkpoints': exception.CheckpointLimitExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def process_reserve_over_quota(context, over_quota_exception,
|
def process_reserve_over_quota(context, over_quota_exception,
|
||||||
|
|
|
@ -53,10 +53,10 @@ class ProvidersApiTest(base.TestCase):
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'karbor.services.protection.api.API.'
|
'karbor.services.protection.api.API.'
|
||||||
'show_checkpoint')
|
'show_checkpoint')
|
||||||
def test_checkpoint_show(self, moak_show_checkpoint):
|
def test_checkpoint_show(self, mock_show_checkpoint):
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
req = fakes.HTTPRequest.blank('/v1/providers/'
|
||||||
'{provider_id}/checkpoints/')
|
'{provider_id}/checkpoints/')
|
||||||
moak_show_checkpoint.return_value = {
|
mock_show_checkpoint.return_value = {
|
||||||
"provider_id": "efc6a88b-9096-4bb6-8634-cda182a6e12a",
|
"provider_id": "efc6a88b-9096-4bb6-8634-cda182a6e12a",
|
||||||
"project_id": "446a04d8-6ff5-4e0e-99a4-827a6389e9ff",
|
"project_id": "446a04d8-6ff5-4e0e-99a4-827a6389e9ff",
|
||||||
"id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
"id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
||||||
|
@ -66,7 +66,7 @@ class ProvidersApiTest(base.TestCase):
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c'
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c'
|
||||||
)
|
)
|
||||||
self.assertTrue(moak_show_checkpoint.called)
|
self.assertTrue(mock_show_checkpoint.called)
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'karbor.services.protection.api.API.'
|
'karbor.services.protection.api.API.'
|
||||||
|
@ -101,54 +101,40 @@ class ProvidersApiTest(base.TestCase):
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c')
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c')
|
||||||
self.assertTrue(moak_list_checkpoints.called)
|
self.assertTrue(moak_list_checkpoints.called)
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch('karbor.quota.QuotaEngine.commit')
|
||||||
'karbor.services.protection.api.API.'
|
@mock.patch('karbor.quota.QuotaEngine.reserve')
|
||||||
'delete')
|
@mock.patch('karbor.services.protection.api.API.show_checkpoint')
|
||||||
def test_checkpoints_delete(self, moak_delete):
|
@mock.patch('karbor.services.protection.api.API.delete')
|
||||||
|
def test_checkpoints_delete(self, mock_delete, mock_show_checkpoint,
|
||||||
|
mock_reserve, mock_commit):
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
req = fakes.HTTPRequest.blank('/v1/providers/'
|
||||||
'{provider_id}/checkpoints/')
|
'{provider_id}/checkpoints/')
|
||||||
|
mock_show_checkpoint.return_value = {
|
||||||
|
"provider_id": "efc6a88b-9096-4bb6-8634-cda182a6e12a",
|
||||||
|
"project_id": "446a04d8-6ff5-4e0e-99a4-827a6389e9ff",
|
||||||
|
"id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
||||||
|
}
|
||||||
self.controller.checkpoints_delete(
|
self.controller.checkpoints_delete(
|
||||||
req,
|
req,
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c')
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c')
|
||||||
self.assertTrue(moak_delete.called)
|
self.assertTrue(mock_delete.called)
|
||||||
|
self.assertTrue(mock_reserve.called)
|
||||||
|
self.assertTrue(mock_commit.called)
|
||||||
|
|
||||||
def test_checkpoints_delete_with_invalid_provider_id(self):
|
@mock.patch('karbor.quota.QuotaEngine.commit')
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
@mock.patch('karbor.quota.QuotaEngine.reserve')
|
||||||
'{provider_id}/checkpoints/')
|
@mock.patch('karbor.services.protection.api.API.protect')
|
||||||
invalid_provider_id = '1'
|
@mock.patch('karbor.objects.plan.Plan.get_by_id')
|
||||||
self.assertRaises(exc.HTTPBadRequest,
|
def test_checkpoints_create(self, mock_plan_get, mock_protect,
|
||||||
self.controller.checkpoints_delete,
|
mock_reserve, mock_commit):
|
||||||
req,
|
|
||||||
invalid_provider_id,
|
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_checkpoints_delete_with_invalid_checkpoint_id(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
|
||||||
'{provider_id}/checkpoints/')
|
|
||||||
invalid_checkpoint_id = '1'
|
|
||||||
self.assertRaises(exc.HTTPBadRequest,
|
|
||||||
self.controller.checkpoints_delete,
|
|
||||||
req,
|
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
|
||||||
invalid_checkpoint_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'karbor.services.protection.api.API.'
|
|
||||||
'protect')
|
|
||||||
@mock.patch(
|
|
||||||
'karbor.objects.plan.Plan.get_by_id')
|
|
||||||
def test_checkpoints_create(self, mock_plan_create,
|
|
||||||
mock_protect):
|
|
||||||
checkpoint = {
|
checkpoint = {
|
||||||
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
||||||
}
|
}
|
||||||
body = {"checkpoint": checkpoint}
|
body = {"checkpoint": checkpoint}
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
req = fakes.HTTPRequest.blank('/v1/providers/'
|
||||||
'{provider_id}/checkpoints/')
|
'{provider_id}/checkpoints/')
|
||||||
mock_plan_create.return_value = {
|
mock_plan_get.return_value = {
|
||||||
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
||||||
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
||||||
}
|
}
|
||||||
|
@ -159,78 +145,57 @@ class ProvidersApiTest(base.TestCase):
|
||||||
req,
|
req,
|
||||||
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
||||||
body=body)
|
body=body)
|
||||||
self.assertTrue(mock_plan_create.called)
|
self.assertTrue(mock_plan_get.called)
|
||||||
|
self.assertTrue(mock_reserve.called)
|
||||||
self.assertTrue(mock_protect.called)
|
self.assertTrue(mock_protect.called)
|
||||||
|
self.assertTrue(mock_commit.called)
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch('karbor.quota.process_reserve_over_quota')
|
||||||
'karbor.services.protection.api.API.'
|
@mock.patch('karbor.quota.QuotaEngine.reserve')
|
||||||
'protect')
|
@mock.patch('karbor.services.protection.api.API.protect')
|
||||||
@mock.patch(
|
@mock.patch('karbor.objects.plan.Plan.get_by_id')
|
||||||
'karbor.objects.plan.Plan.get_by_id')
|
def test_checkpoints_create_with_over_quota_exception(
|
||||||
def test_checkpoints_create_with_invalid_provider_id(self,
|
self, mock_plan_get, mock_protect, mock_quota_reserve,
|
||||||
mock_plan_create,
|
mock_process_reserve_over_quota):
|
||||||
mock_protect):
|
checkpoint = {"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"}
|
||||||
checkpoint = {
|
|
||||||
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
|
||||||
}
|
|
||||||
body = {"checkpoint": checkpoint}
|
body = {"checkpoint": checkpoint}
|
||||||
req = fakes.HTTPRequest.blank('/v1/providers/'
|
req = fakes.HTTPRequest.blank('/v1/providers/'
|
||||||
'{provider_id}/checkpoints/')
|
'{provider_id}/checkpoints/')
|
||||||
mock_plan_create.return_value = {
|
mock_plan_get.return_value = {
|
||||||
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
||||||
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
||||||
}
|
}
|
||||||
mock_protect.return_value = {
|
mock_protect.return_value = {
|
||||||
"checkpoint_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
"checkpoint_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
||||||
}
|
}
|
||||||
invalid_provider_id = None
|
mock_quota_reserve.side_effect = exception.OverQuota
|
||||||
self.assertRaises(exception.InvalidInput,
|
self.controller.checkpoints_create(
|
||||||
self.controller.checkpoints_create, req,
|
req,
|
||||||
invalid_provider_id,
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
||||||
body=body)
|
body=body)
|
||||||
|
self.assertTrue(mock_process_reserve_over_quota.called)
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch('karbor.quota.QuotaEngine.rollback')
|
||||||
'karbor.services.protection.api.API.'
|
@mock.patch('karbor.services.protection.api.API.protect')
|
||||||
'protect')
|
@mock.patch('karbor.objects.plan.Plan.get_by_id')
|
||||||
@mock.patch(
|
def test_checkpoint_create_failed_with_protection_exception(
|
||||||
'karbor.objects.plan.Plan.get_by_id')
|
self, mock_plan_get, mock_protect, mock_quota_rollback):
|
||||||
def test_checkpoints_create_with_non_exist_plan(self,
|
|
||||||
mock_plan_create,
|
|
||||||
mock_protect):
|
|
||||||
checkpoint = {
|
|
||||||
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
|
||||||
}
|
|
||||||
body = {"checkpoint": checkpoint}
|
|
||||||
req = fakes.HTTPRequest.blank(
|
|
||||||
'/v1/providers/{provider_id}/checkpoints/')
|
|
||||||
mock_plan_create.return_value = None
|
|
||||||
mock_protect.return_value = {
|
|
||||||
"checkpoint_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"
|
|
||||||
}
|
|
||||||
self.assertRaises(exception.PlanNotFound,
|
|
||||||
self.controller.checkpoints_create, req,
|
|
||||||
"2220f8b1-975d-4621-a872-fa9afb43cb6c", body=body)
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'karbor.services.protection.api.API.protect')
|
|
||||||
@mock.patch(
|
|
||||||
'karbor.objects.plan.Plan.get_by_id')
|
|
||||||
def test_checkpoints_create_with_invalid_plan(self,
|
|
||||||
mock_plan_create,
|
|
||||||
mock_protect):
|
|
||||||
checkpoint = {"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"}
|
checkpoint = {"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"}
|
||||||
body = {"checkpoint": checkpoint}
|
body = {"checkpoint": checkpoint}
|
||||||
req = fakes.HTTPRequest.blank(
|
req = fakes.HTTPRequest.blank('/v1/providers/'
|
||||||
'/v1/providers/{provider_id}/checkpoints/')
|
'{provider_id}/checkpoints/')
|
||||||
mock_plan_create.return_value = \
|
mock_plan_get.return_value = {
|
||||||
{"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
"plan_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6",
|
||||||
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"}
|
"provider_id": "2220f8b1-975d-4621-a872-fa9afb43cb6c"
|
||||||
mock_protect.return_value = \
|
}
|
||||||
{"checkpoint_id": "2c3a12ee-5ea6-406a-8b64-862711ff85e6"}
|
mock_protect.side_effect = Exception
|
||||||
self.assertRaises(exception.InvalidPlan,
|
self.assertRaises(
|
||||||
|
exc.HTTPBadRequest,
|
||||||
self.controller.checkpoints_create,
|
self.controller.checkpoints_create,
|
||||||
req, "2220f8b1-5ea6-4621-a872-fa9afb43cb6c",
|
req,
|
||||||
|
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
|
||||||
body=body)
|
body=body)
|
||||||
|
self.assertTrue(mock_quota_rollback.called)
|
||||||
|
|
||||||
@mock.patch('karbor.services.protection.api.API.reset_state')
|
@mock.patch('karbor.services.protection.api.API.reset_state')
|
||||||
def test_checkpoints_update_reset_state(self, mock_reset_state):
|
def test_checkpoints_update_reset_state(self, mock_reset_state):
|
||||||
|
|
Loading…
Reference in New Issue