Nested Quota Driver: Get Project Hierarchy

We are making calls to keystone in order to know the project hierarchy
and check whether a project is a root project or not.

Co-Authored-By: Vilobh Meshram <vilobhmm@yahoo-inc.com>

Change-Id: Ic749fd56d7c6b41f720f8e86bf62066c6a63122b
Partially-Implements: bp cinder-nested-quota-driver
This commit is contained in:
Erickson Santos 2015-07-27 15:01:47 -03:00 committed by Vilobh Meshram
parent f6dafdaa24
commit d8bd178465
2 changed files with 131 additions and 7 deletions

View File

@ -13,9 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo_utils import strutils
import webob import webob
from keystoneclient import exceptions
from keystoneclient.v3 import client
from cinder.api import extensions from cinder.api import extensions
from cinder.api.openstack import wsgi from cinder.api.openstack import wsgi
from cinder.api import xmlutil from cinder.api import xmlutil
@ -26,7 +28,11 @@ from cinder.i18n import _
from cinder import quota from cinder import quota
from cinder import utils from cinder import utils
from oslo_config import cfg
from oslo_utils import strutils
CONF = cfg.CONF
QUOTAS = quota.QUOTAS QUOTAS = quota.QUOTAS
NON_QUOTA_KEYS = ['tenant_id', 'id'] NON_QUOTA_KEYS = ['tenant_id', 'id']
@ -75,6 +81,23 @@ class QuotaSetsController(wsgi.Controller):
else: else:
return {k: v['limit'] for k, v in values.items()} return {k: v['limit'] for k, v in values.items()}
def _get_project(self, context, id, subtree_as_ids=False):
"""A Helper method to get the project hierarchy.
Along with Hierachical Multitenancy, projects can be hierarchically
organized. Therefore, we need to know the project hierarchy, if any, in
order to do quota operations properly.
"""
try:
keystone = client.Client(auth_url=CONF.keymgr.encryption_auth_url,
token=context.auth_token,
project_id=context.project_id)
project = keystone.projects.get(id, subtree_as_ids=subtree_as_ids)
except exceptions.NotFound:
msg = (_("Tenant ID: %s does not exist.") % id)
raise webob.exc.HTTPNotFound(explanation=msg)
return project
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
def show(self, req, id): def show(self, req, id):
context = req.environ['cinder.context'] context = req.environ['cinder.context']
@ -159,10 +182,9 @@ class QuotaSetsController(wsgi.Controller):
def defaults(self, req, id): def defaults(self, req, id):
context = req.environ['cinder.context'] context = req.environ['cinder.context']
authorize_show(context) authorize_show(context)
return self._format_quota_set(id, project = self._get_project(context, context.project_id)
QUOTAS.get_defaults(context, return self._format_quota_set(id, QUOTAS.get_defaults(
parent_project_id= context, parent_project_id=project.parent_id))
None))
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
def delete(self, req, id): def delete(self, req, id):

View File

@ -22,8 +22,9 @@ Tests for cinder.api.contrib.quotas.py
import mock import mock
from lxml import etree from lxml import etree
import webob.exc
import uuid
import webob.exc
from cinder.api.contrib import quotas from cinder.api.contrib import quotas
from cinder import context from cinder import context
@ -31,6 +32,11 @@ from cinder import db
from cinder import test from cinder import test
from cinder.tests.unit import test_db_api from cinder.tests.unit import test_db_api
from oslo_config import cfg
CONF = cfg.CONF
def make_body(root=True, gigabytes=1000, snapshots=10, def make_body(root=True, gigabytes=1000, snapshots=10,
volumes=10, backups=10, backup_gigabytes=1000, volumes=10, backups=10, backup_gigabytes=1000,
@ -57,8 +63,24 @@ def make_body(root=True, gigabytes=1000, snapshots=10,
return result return result
def make_subproject_body(root=True, gigabytes=0, snapshots=0,
volumes=0, backups=0, backup_gigabytes=0,
tenant_id='foo', per_volume_gigabytes=0):
return make_body(root=root, gigabytes=gigabytes, snapshots=snapshots,
volumes=volumes, backups=backups,
backup_gigabytes=backup_gigabytes, tenant_id=tenant_id,
per_volume_gigabytes=per_volume_gigabytes)
class QuotaSetsControllerTest(test.TestCase): class QuotaSetsControllerTest(test.TestCase):
class FakeProject(object):
def __init__(self, id='foo', parent_id=None):
self.id = id
self.parent_id = parent_id
self.subtree = None
def setUp(self): def setUp(self):
super(QuotaSetsControllerTest, self).setUp() super(QuotaSetsControllerTest, self).setUp()
self.controller = quotas.QuotaSetsController() self.controller = quotas.QuotaSetsController()
@ -66,16 +88,80 @@ class QuotaSetsControllerTest(test.TestCase):
self.req = self.mox.CreateMockAnything() self.req = self.mox.CreateMockAnything()
self.req.environ = {'cinder.context': context.get_admin_context()} self.req.environ = {'cinder.context': context.get_admin_context()}
self.req.environ['cinder.context'].is_admin = True self.req.environ['cinder.context'].is_admin = True
self.req.environ['cinder.context'].auth_token = uuid.uuid4().hex
self._create_project_hierarchy()
self.auth_url = CONF.keymgr.encryption_auth_url
def _create_project_hierarchy(self):
"""Sets an environment used for nested quotas tests.
Create a project hierarchy such as follows:
+-----------+
| |
| A |
| / \ |
| B C |
| / |
| D |
+-----------+
"""
self.A = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
self.B = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.A.id)
self.C = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.A.id)
self.D = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.B.id)
# update projects subtrees
self.B.subtree = {self.D.id: self.D.subtree}
self.A.subtree = {self.B.id: self.B.subtree, self.C.id: self.C.subtree}
# project_by_id attribute is used to recover a project based on its id.
self.project_by_id = {self.A.id: self.A, self.B.id: self.B,
self.C.id: self.C, self.D.id: self.D}
def _get_project(self, context, id, subtree_as_ids=False):
return self.project_by_id.get(id, self.FakeProject())
@mock.patch('keystoneclient.v3.client.Client')
def test_keystone_client_instantiation(self, ksclient_class):
context = self.req.environ['cinder.context']
self.controller._get_project(context, context.project_id)
ksclient_class.assert_called_once_with(auth_url=self.auth_url,
token=context.auth_token,
project_id=context.project_id)
@mock.patch('keystoneclient.v3.client.Client')
def test_get_project(self, ksclient_class):
context = self.req.environ['cinder.context']
keystoneclient = ksclient_class.return_value
self.controller._get_project(context, context.project_id)
keystoneclient.projects.get.assert_called_once_with(
context.project_id, subtree_as_ids=False)
def test_defaults(self): def test_defaults(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result = self.controller.defaults(self.req, 'foo') result = self.controller.defaults(self.req, 'foo')
self.assertDictMatch(result, make_body()) self.assertDictMatch(result, make_body())
def test_subproject_defaults(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
context = self.req.environ['cinder.context']
context.project_id = self.B.id
result = self.controller.defaults(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(result, expected)
def test_show(self): def test_show(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result = self.controller.show(self.req, 'foo') result = self.controller.show(self.req, 'foo')
self.assertDictMatch(result, make_body()) self.assertDictMatch(result, make_body())
def test_show_not_authorized(self): def test_show_not_authorized(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False self.req.environ['cinder.context'].is_admin = False
self.req.environ['cinder.context'].user_id = 'bad_user' self.req.environ['cinder.context'].user_id = 'bad_user'
self.req.environ['cinder.context'].project_id = 'bad_project' self.req.environ['cinder.context'].project_id = 'bad_project'
@ -83,6 +169,8 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo') self.req, 'foo')
def test_update(self): def test_update(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = make_body(gigabytes=2000, snapshots=15, body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None) volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, 'foo', body) result = self.controller.update(self.req, 'foo', body)
@ -112,16 +200,22 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo', body) self.req, 'foo', body)
def test_update_invalid_value_key_value(self): def test_update_invalid_value_key_value(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': "should_be_int"}} body = {'quota_set': {'gigabytes': "should_be_int"}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
def test_update_invalid_type_key_value(self): def test_update_invalid_type_key_value(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': None}} body = {'quota_set': {'gigabytes': None}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
def test_update_multi_value_with_bad_data(self): def test_update_multi_value_with_bad_data(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
orig_quota = self.controller.show(self.req, 'foo') orig_quota = self.controller.show(self.req, 'foo')
body = make_body(gigabytes=2000, snapshots=15, volumes="should_be_int", body = make_body(gigabytes=2000, snapshots=15, volumes="should_be_int",
backups=5, tenant_id=None) backups=5, tenant_id=None)
@ -132,6 +226,8 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertDictMatch(orig_quota, new_quota) self.assertDictMatch(orig_quota, new_quota)
def test_update_bad_quota_limit(self): def test_update_bad_quota_limit(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': -1000}} body = {'quota_set': {'gigabytes': -1000}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
@ -140,6 +236,8 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo', body) self.req, 'foo', body)
def test_update_no_admin(self): def test_update_no_admin(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False self.req.environ['cinder.context'].is_admin = False
self.req.environ['cinder.context'].project_id = 'foo' self.req.environ['cinder.context'].project_id = 'foo'
self.req.environ['cinder.context'].user_id = 'foo_user' self.req.environ['cinder.context'].user_id = 'foo_user'
@ -195,6 +293,8 @@ class QuotaSetsControllerTest(test.TestCase):
result['quota_set']['volumes']) result['quota_set']['volumes'])
def test_delete(self): def test_delete(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result_show = self.controller.show(self.req, 'foo') result_show = self.controller.show(self.req, 'foo')
self.assertDictMatch(result_show, make_body()) self.assertDictMatch(result_show, make_body())
@ -210,9 +310,11 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertDictMatch(result_show, result_show_after) self.assertDictMatch(result_show, result_show_after)
def test_delete_no_admin(self): def test_delete_no_admin(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False self.req.environ['cinder.context'].is_admin = False
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
self.req, 'test') self.req, 'foo')
class QuotaSerializerTest(test.TestCase): class QuotaSerializerTest(test.TestCase):