diff options
author | Thiago Paiva Brito <thiagop@lsd.ufcg.edu.br> | 2014-08-20 16:26:00 -0300 |
---|---|---|
committer | Rodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br> | 2015-02-05 22:36:47 -0300 |
commit | 8da68e3ded4aa7208f5bc55cf4f438e4d0e910b1 (patch) | |
tree | f9cca18a304d1d8ae5109b8bbb65aaece7216ee0 | |
parent | cef7775cda6d4113171ff713ee36b93731b89242 (diff) | |
download | python-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.py | 178 | ||||
-rw-r--r-- | keystoneclient/tests/v3/utils.py | 3 | ||||
-rw-r--r-- | keystoneclient/v3/projects.py | 52 |
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, |