summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThiago Paiva Brito <thiagop@lsd.ufcg.edu.br>2014-08-20 16:26:00 -0300
committerRodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br>2015-02-05 22:36:47 -0300
commit8da68e3ded4aa7208f5bc55cf4f438e4d0e910b1 (patch)
treef9cca18a304d1d8ae5109b8bbb65aaece7216ee0
parentcef7775cda6d4113171ff713ee36b93731b89242 (diff)
downloadpython-keystoneclient-8da68e3ded4aa7208f5bc55cf4f438e4d0e910b1.tar.gz
Hierarchical multitenancy basic calls
This patch addresses changes needed to manage projects through keystoneclient API v3. The changes are: create: new param 'parent': set the parent project of the project being created get: new param 'subtree_as_list': If True, shows projects down the hierarchy new param 'parents_as_list': If True, shows projects up the hierarchy Co-Authored-By: Andre Aranha <afaranha@lsd.ufcg.edu.br> Co-Authored-By: Rodrigo Duarte <rodrigods@lsd.ufcg.edu.br> Change-Id: I0f02a66e6a29584197ed00cb32caecb50956f458 Implements: blueprint hierarchical-multitenancy
-rw-r--r--keystoneclient/tests/v3/test_projects.py178
-rw-r--r--keystoneclient/tests/v3/utils.py3
-rw-r--r--keystoneclient/v3/projects.py52
3 files changed, 225 insertions, 8 deletions
diff --git a/keystoneclient/tests/v3/test_projects.py b/keystoneclient/tests/v3/test_projects.py
index 8087e42..4c38208 100644
--- a/keystoneclient/tests/v3/test_projects.py
+++ b/keystoneclient/tests/v3/test_projects.py
@@ -12,6 +12,7 @@
import uuid
+from keystoneclient import exceptions
from keystoneclient.tests.v3 import utils
from keystoneclient.v3 import projects
@@ -26,10 +27,14 @@ class ProjectTests(utils.TestCase, utils.CrudTests):
def new_ref(self, **kwargs):
kwargs = super(ProjectTests, self).new_ref(**kwargs)
- kwargs.setdefault('domain_id', uuid.uuid4().hex)
- kwargs.setdefault('enabled', True)
- kwargs.setdefault('name', uuid.uuid4().hex)
- return kwargs
+ return self._new_project_ref(ref=kwargs)
+
+ def _new_project_ref(self, ref=None):
+ ref = ref or {}
+ ref.setdefault('domain_id', uuid.uuid4().hex)
+ ref.setdefault('enabled', True)
+ ref.setdefault('name', uuid.uuid4().hex)
+ return ref
def test_list_projects_for_user(self):
ref_list = [self.new_ref(), self.new_ref()]
@@ -55,3 +60,168 @@ class ProjectTests(utils.TestCase, utils.CrudTests):
[self.assertIsInstance(r, self.model) for r in returned_list]
self.assertQueryStringIs('domain_id=%s' % domain_id)
+
+ def test_create_with_parent(self):
+ parent_ref = self.new_ref()
+ parent_ref['parent_id'] = uuid.uuid4().hex
+ parent = self.test_create(ref=parent_ref)
+ parent.id = parent_ref['id']
+
+ # Create another project under 'parent' in the hierarchy
+ ref = self.new_ref()
+ ref['parent_id'] = parent.id
+
+ child_ref = ref.copy()
+ del child_ref['parent_id']
+ child_ref['parent'] = parent
+
+ # test_create() pops the 'id' of the mocked response
+ del ref['id']
+
+ # Resource objects may peform lazy-loading. The create() method of
+ # ProjectManager will try to access the 'uuid' attribute of the parent
+ # object, which will trigger a call to fetch the Resource attributes.
+ self.stub_entity('GET', id=parent_ref['id'], entity=parent_ref)
+ self.test_create(ref=child_ref, req_ref=ref)
+
+ def test_create_with_parent_id(self):
+ ref = self._new_project_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('POST', entity=ref, status_code=201)
+
+ returned = self.manager.create(name=ref['name'],
+ domain=ref['domain_id'],
+ parent_id=ref['parent_id'])
+
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(ref)
+
+ def test_create_with_parent_and_parent_id(self):
+ ref = self._new_project_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('POST', entity=ref, status_code=201)
+
+ # Should ignore the 'parent_id' argument since we are also passing
+ # 'parent'
+ returned = self.manager.create(name=ref['name'],
+ domain=ref['domain_id'],
+ parent=ref['parent_id'],
+ parent_id=uuid.uuid4().hex)
+
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(ref)
+
+ def _create_projects_hierarchy(self, hierarchy_size=3):
+ """Creates a project hierarchy with specified size.
+
+ :param hierarchy_size: the desired hierarchy size, default is 3.
+
+ :returns: a list of the projects in the created hierarchy.
+
+ """
+
+ ref = self.new_ref()
+ project_id = ref['id']
+ projects = [ref]
+
+ for i in range(1, hierarchy_size):
+ new_ref = self.new_ref()
+ new_ref['parent_id'] = project_id
+ projects.append(new_ref)
+ project_id = new_ref['id']
+
+ return projects
+
+ def test_get_with_subtree_as_list(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[0]
+
+ ref['subtree_as_list'] = []
+ for i in range(1, len(projects)):
+ ref['subtree_as_list'].append(projects[i])
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], subtree_as_list=True)
+ self.assertQueryStringIs('subtree_as_list')
+ for i in range(1, len(projects)):
+ for attr in projects[i]:
+ child = getattr(returned, 'subtree_as_list')[i - 1]
+ self.assertEqual(
+ child[attr],
+ projects[i][attr],
+ 'Expected different %s' % attr)
+
+ def test_get_with_parents_as_list(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[2]
+
+ ref['parents_as_list'] = []
+ for i in range(0, len(projects) - 1):
+ ref['parents_as_list'].append(projects[i])
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], parents_as_list=True)
+ self.assertQueryStringIs('parents_as_list')
+ for i in range(0, len(projects) - 1):
+ for attr in projects[i]:
+ parent = getattr(returned, 'parents_as_list')[i]
+ self.assertEqual(
+ parent[attr],
+ projects[i][attr],
+ 'Expected different %s' % attr)
+
+ def test_get_with_parents_as_list_and_subtree_as_list(self):
+ ref = self.new_ref()
+ projects = self._create_projects_hierarchy()
+ ref = projects[1]
+
+ ref['parents_as_list'] = [projects[0]]
+ ref['subtree_as_list'] = [projects[2]]
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'],
+ parents_as_list=True,
+ subtree_as_list=True)
+ self.assertQueryStringIs('subtree_as_list&parents_as_list')
+
+ for attr in projects[0]:
+ parent = getattr(returned, 'parents_as_list')[0]
+ self.assertEqual(
+ parent[attr],
+ projects[0][attr],
+ 'Expected different %s' % attr)
+
+ for attr in projects[2]:
+ child = getattr(returned, 'subtree_as_list')[0]
+ self.assertEqual(
+ child[attr],
+ projects[2][attr],
+ 'Expected different %s' % attr)
+
+ def test_update_with_parent_project(self):
+ ref = self.new_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('PATCH', id=ref['id'], entity=ref, status_code=403)
+ req_ref = ref.copy()
+ req_ref.pop('id')
+
+ # NOTE(rodrigods): this is the expected behaviour of the Identity
+ # server, a different implementation might not fail this request.
+ self.assertRaises(exceptions.Forbidden, self.manager.update,
+ ref['id'], **utils.parameterize(req_ref))
diff --git a/keystoneclient/tests/v3/utils.py b/keystoneclient/tests/v3/utils.py
index 8c76135..f1b9934 100644
--- a/keystoneclient/tests/v3/utils.py
+++ b/keystoneclient/tests/v3/utils.py
@@ -219,6 +219,9 @@ class CrudTests(object):
'Expected different %s' % attr)
self.assertEntityRequestBodyIs(req_ref)
+ # The entity created here may be used in other test cases
+ return returned
+
def test_get(self, ref=None):
ref = ref or self.new_ref()
diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py
index 2fcd249..0a98991 100644
--- a/keystoneclient/v3/projects.py
+++ b/keystoneclient/v3/projects.py
@@ -26,6 +26,11 @@ class Project(base.Resource):
* name: project name
* description: project description
* enabled: boolean to indicate if project is enabled
+ * parent_id: a uuid representing this project's parent in hierarchy
+ * parents: a list or a structured dict containing the parents of this
+ project in the hierarchy
+ * subtree: a list or a structured dict containing the subtree of this
+ project in the hierarchy
"""
@utils.positional(enforcement=utils.positional.WARN)
@@ -54,7 +59,26 @@ class ProjectManager(base.CrudManager):
key = 'project'
@utils.positional(1, enforcement=utils.positional.WARN)
- def create(self, name, domain, description=None, enabled=True, **kwargs):
+ def create(self, name, domain, description=None,
+ enabled=True, parent=None, **kwargs):
+ """Create a project.
+
+ :param str name: project name.
+ :param domain: the project domain.
+ :type domain: :py:class:`keystoneclient.v3.domains.Domain` or str
+ :param str description: the project description. (optional)
+ :param boolean enabled: if the project is enabled. (optional)
+ :param parent: the project's parent in the hierarchy. (optional)
+ :type parent: :py:class:`keystoneclient.v3.projects.Project` or str
+ """
+
+ # NOTE(rodrigods): the API must be backwards compatible, so if an
+ # application was passing a 'parent_id' before as kwargs, the call
+ # should not fail. If both 'parent' and 'parent_id' are provided,
+ # 'parent' will be preferred.
+ if parent:
+ kwargs['parent_id'] = base.getid(parent)
+
return super(ProjectManager, self).create(
domain_id=base.getid(domain),
name=name,
@@ -79,9 +103,29 @@ class ProjectManager(base.CrudManager):
fallback_to_auth=True,
**kwargs)
- def get(self, project):
- return super(ProjectManager, self).get(
- project_id=base.getid(project))
+ @utils.positional()
+ def get(self, project, subtree_as_list=False, parents_as_list=False):
+ """Get a project.
+
+ :param project: project to be retrieved.
+ :type project: :py:class:`keystoneclient.v3.projects.Project` or str
+ :param boolean subtree_as_list: retrieve projects below this project
+ in the hierarchy as a flat list.
+ (optional)
+ :param boolean parents_as_list: retrieve projects above this project
+ in the hierarchy as a flat list.
+ (optional)
+ """
+ # According to the API spec, the query params are key only
+ query = ''
+ if subtree_as_list:
+ query = '?subtree_as_list'
+ if parents_as_list:
+ query = query + '&parents_as_list' if query else '?parents_as_list'
+
+ dict_args = {'project_id': base.getid(project)}
+ url = self.build_url(dict_args_in_out=dict_args) + query
+ return self._get(url, self.key)
@utils.positional(enforcement=utils.positional.WARN)
def update(self, project, name=None, domain=None, description=None,