diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index 2f0300485f17..4d82ed4b81f0 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -21,3 +21,4 @@ def register_all(): __import__('nova.objects.instance_info_cache') __import__('nova.objects.security_group') __import__('nova.objects.migration') + __import__('nova.objects.quotas') diff --git a/nova/objects/base.py b/nova/objects/base.py index 2b83ec2e6c8e..b3e3a54768a5 100644 --- a/nova/objects/base.py +++ b/nova/objects/base.py @@ -129,6 +129,8 @@ def remotable(fn): if ctxt is None: raise exception.OrphanedObjectError(method=fn.__name__, objtype=self.obj_name()) + # Force this to be set if it wasn't before. + self._context = ctxt if NovaObject.indirection_api: updates, result = NovaObject.indirection_api.object_action( ctxt, self, fn.__name__, args, kwargs) diff --git a/nova/objects/quotas.py b/nova/objects/quotas.py new file mode 100644 index 000000000000..68814b682912 --- /dev/null +++ b/nova/objects/quotas.py @@ -0,0 +1,101 @@ +# Copyright 2013 Rackspace Hosting. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from nova.objects import base +from nova.objects import utils as obj_utils +from nova import quota + + +def ids_from_instance(context, instance): + if (context.is_admin and + context.project_id != instance['project_id']): + project_id = instance['project_id'] + else: + project_id = context.project_id + if context.user_id != instance['user_id']: + user_id = instance['user_id'] + else: + user_id = context.user_id + return project_id, user_id + + +class Quotas(base.NovaObject): + fields = { + 'reservations': obj_utils.list_of_strings_or_none, + 'project_id': obj_utils.str_or_none, + 'user_id': obj_utils.str_or_none, + } + + def __init__(self): + super(Quotas, self).__init__() + self.quotas = quota.QUOTAS + # Set up defaults. + self.reservations = [] + self.project_id = None + self.user_id = None + self.obj_reset_changes() + + @classmethod + def from_reservations(cls, context, reservations, instance=None): + """Transitional for compatibility.""" + if instance is None: + project_id = None + user_id = None + else: + project_id, user_id = ids_from_instance(context, instance) + quotas = cls() + quotas._context = context + quotas.reservations = reservations + quotas.project_id = project_id + quotas.user_id = user_id + quotas.obj_reset_changes() + return quotas + + @base.remotable + def reserve(self, context, expire=None, project_id=None, user_id=None, + **deltas): + reservations = self.quotas.reserve(context, expire=expire, + project_id=project_id, + user_id=user_id, + **deltas) + self.reservations = reservations + self.project_id = project_id + self.user_id = user_id + self.obj_reset_changes() + + @base.remotable + def commit(self, context=None): + if not self.reservations: + return + if context is None: + context = self._context + self.quotas.commit(context, self.reservations, + project_id=self.project_id, + user_id=self.user_id) + self.reservations = None + self.obj_reset_changes() + + @base.remotable + def rollback(self, context=None): + """Rollback quotas.""" + if not self.reservations: + return + if context is None: + context = self._context + self.quotas.rollback(context, self.reservations, + project_id=self.project_id, + user_id=self.user_id) + self.reservations = None + self.obj_reset_changes() diff --git a/nova/tests/objects/test_quotas.py b/nova/tests/objects/test_quotas.py new file mode 100644 index 000000000000..768086a17e51 --- /dev/null +++ b/nova/tests/objects/test_quotas.py @@ -0,0 +1,152 @@ +# Copyright 2013 Rackspace Hosting. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import context +from nova.objects import quotas as quotas_obj +from nova import quota +from nova import test +from nova.tests import fake_instance +from nova.tests.objects import test_objects + + +QUOTAS = quota.QUOTAS + + +class TestQuotasModule(test.NoDBTestCase): + def setUp(self): + super(TestQuotasModule, self).setUp() + self.context = context.RequestContext('fake_user1', 'fake_proj1') + self.instance = fake_instance.fake_db_instance( + project_id='fake_proj2', user_id='fake_user2') + + def test_ids_from_instance_non_admin(self): + project_id, user_id = quotas_obj.ids_from_instance( + self.context, self.instance) + self.assertEqual('fake_user2', user_id) + self.assertEqual('fake_proj1', project_id) + + def test_ids_from_instance_admin(self): + project_id, user_id = quotas_obj.ids_from_instance( + self.context.elevated(), self.instance) + self.assertEqual('fake_user2', user_id) + self.assertEqual('fake_proj2', project_id) + + +class _TestQuotasObject(object): + def setUp(self): + super(_TestQuotasObject, self).setUp() + print 'got here' + self.context = context.RequestContext('fake_user1', 'fake_proj1') + self.instance = fake_instance.fake_db_instance( + project_id='fake_proj2', user_id='fake_user2') + + def test_from_reservations(self): + fake_reservations = ['1', '2'] + quotas = quotas_obj.Quotas.from_reservations( + self.context, fake_reservations) + self.assertEqual(self.context, quotas._context) + self.assertEqual(fake_reservations, quotas.reservations) + self.assertEqual(None, quotas.project_id) + self.assertEqual(None, quotas.user_id) + + def test_from_reservations_bogus(self): + fake_reservations = [1, 2] + self.assertRaises(ValueError, + quotas_obj.Quotas.from_reservations, + self.context, fake_reservations) + + def test_from_reservations_instance(self): + fake_reservations = ['1', '2'] + quotas = quotas_obj.Quotas.from_reservations( + self.context, fake_reservations, + instance=self.instance) + self.assertEqual(self.context, quotas._context) + self.assertEqual(fake_reservations, quotas.reservations) + self.assertEqual('fake_proj1', quotas.project_id) + self.assertEqual('fake_user2', quotas.user_id) + + def test_from_reservations_instance_admin(self): + fake_reservations = ['1', '2'] + elevated = self.context.elevated() + quotas = quotas_obj.Quotas.from_reservations( + elevated, fake_reservations, + instance=self.instance) + self.assertEqual(elevated, quotas._context) + self.assertEqual(fake_reservations, quotas.reservations) + self.assertEqual('fake_proj2', quotas.project_id) + self.assertEqual('fake_user2', quotas.user_id) + + def test_reserve(self): + fake_reservations = ['1', '2'] + quotas = quotas_obj.Quotas() + + self.mox.StubOutWithMock(QUOTAS, 'reserve') + QUOTAS.reserve(self.context, expire='expire', + project_id='project_id', user_id='user_id', + moo='cow').AndReturn(fake_reservations) + + self.mox.ReplayAll() + quotas.reserve(self.context, expire='expire', + project_id='project_id', user_id='user_id', + moo='cow') + self.assertEqual(self.context, quotas._context) + self.assertEqual(fake_reservations, quotas.reservations) + self.assertEqual('project_id', quotas.project_id) + self.assertEqual('user_id', quotas.user_id) + + def test_commit(self): + fake_reservations = ['1', '2'] + quotas = quotas_obj.Quotas.from_reservations( + self.context, fake_reservations) + + self.mox.StubOutWithMock(QUOTAS, 'commit') + QUOTAS.commit(self.context, fake_reservations, + project_id=None, user_id=None) + + self.mox.ReplayAll() + quotas.commit() + self.assertEqual(None, quotas.reservations) + + def test_commit_none_reservations(self): + quotas = quotas_obj.Quotas.from_reservations(self.context, None) + self.mox.StubOutWithMock(QUOTAS, 'commit') + self.mox.ReplayAll() + quotas.commit() + + def test_rollback(self): + fake_reservations = ['1', '2'] + quotas = quotas_obj.Quotas.from_reservations( + self.context, fake_reservations) + + self.mox.StubOutWithMock(QUOTAS, 'rollback') + QUOTAS.rollback(self.context, fake_reservations, + project_id=None, user_id=None) + + self.mox.ReplayAll() + quotas.rollback() + self.assertEqual(None, quotas.reservations) + + def test_rollback_none_reservations(self): + quotas = quotas_obj.Quotas.from_reservations(self.context, None) + self.mox.StubOutWithMock(QUOTAS, 'rollback') + self.mox.ReplayAll() + quotas.rollback() + + +class TestQuotasObject(_TestQuotasObject, test_objects._LocalTest): + pass + + +class TestRemoteQuotasObject(_TestQuotasObject, test_objects._RemoteTest): + pass