Skip to content

Commit 2ed0e22

Browse files
committed
Add parent field to project creation
Adding the possibility to create projects hierarchies by adding the parent field in the create project call. Co-Authored-By: Victor Silva <victor@lsd.ufcg.edu.br> Implements: bp hierarchical-multitenancy Change-Id: I4eac4f5bc067634cc38c305dacc59ab1da63c153
1 parent a8c4407 commit 2ed0e22

4 files changed

Lines changed: 129 additions & 2 deletions

File tree

‎doc/source/command-objects/project.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ Create new project
2525

2626
.. versionadded:: 3
2727

28+
.. option:: --parent <project>
29+
30+
Parent of the project (name or ID)
31+
32+
.. versionadded:: 3
33+
2834
.. option:: --description <description>
2935

3036
Project description

‎openstackclient/identity/v3/project.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ def get_parser(self, prog_name):
4646
metavar='<domain>',
4747
help='Domain owning the project (name or ID)',
4848
)
49+
parser.add_argument(
50+
'--parent',
51+
metavar='<project>',
52+
help='Parent of the project (name or ID)',
53+
)
4954
parser.add_argument(
5055
'--description',
5156
metavar='<description>',
@@ -86,6 +91,13 @@ def take_action(self, parsed_args):
8691
else:
8792
domain = None
8893

94+
parent = None
95+
if parsed_args.parent:
96+
parent = utils.find_resource(
97+
identity_client.projects,
98+
parsed_args.parent,
99+
).id
100+
89101
enabled = True
90102
if parsed_args.disable:
91103
enabled = False
@@ -97,6 +109,7 @@ def take_action(self, parsed_args):
97109
project = identity_client.projects.create(
98110
name=parsed_args.name,
99111
domain=domain,
112+
parent=parent,
100113
description=parsed_args.description,
101114
enabled=enabled,
102115
**kwargs
@@ -111,8 +124,6 @@ def take_action(self, parsed_args):
111124
raise e
112125

113126
project._info.pop('links')
114-
# TODO(stevemar): Remove the line below when we support multitenancy
115-
project._info.pop('parent_id', None)
116127
return zip(*sorted(six.iteritems(project._info)))
117128

118129

‎openstackclient/tests/identity/v3/fakes.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@
135135
'links': base_url + 'regions/' + region_id,
136136
}
137137

138+
PROJECT_WITH_PARENT = {
139+
'id': project_id + '-with-parent',
140+
'name': project_name + ' and their parents',
141+
'description': project_description + ' plus another four',
142+
'enabled': True,
143+
'domain_id': domain_id,
144+
'parent_id': project_id,
145+
'links': base_url + 'projects/' + (project_id + '-with-parent'),
146+
}
147+
138148
role_id = 'r1'
139149
role_name = 'roller'
140150

‎openstackclient/tests/identity/v3/test_project.py‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import copy
1717
import mock
1818

19+
from openstackclient.common import exceptions
1920
from openstackclient.identity.v3 import project
2021
from openstackclient.tests import fakes
2122
from openstackclient.tests.identity.v3 import fakes as identity_fakes
@@ -60,6 +61,7 @@ def test_project_create_no_options(self):
6061
identity_fakes.project_name,
6162
]
6263
verifylist = [
64+
('parent', None),
6365
('enable', False),
6466
('disable', False),
6567
('name', identity_fakes.project_name),
@@ -75,6 +77,7 @@ def test_project_create_no_options(self):
7577
'domain': None,
7678
'description': None,
7779
'enabled': True,
80+
'parent': None,
7881
}
7982
# ProjectManager.create(name=, domain=, description=,
8083
# enabled=, **kwargs)
@@ -103,6 +106,7 @@ def test_project_create_description(self):
103106
('enable', False),
104107
('disable', False),
105108
('name', identity_fakes.project_name),
109+
('parent', None),
106110
]
107111
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
108112

@@ -115,6 +119,7 @@ def test_project_create_description(self):
115119
'domain': None,
116120
'description': 'new desc',
117121
'enabled': True,
122+
'parent': None,
118123
}
119124
# ProjectManager.create(name=, domain=, description=,
120125
# enabled=, **kwargs)
@@ -143,6 +148,7 @@ def test_project_create_domain(self):
143148
('enable', False),
144149
('disable', False),
145150
('name', identity_fakes.project_name),
151+
('parent', None),
146152
]
147153
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
148154

@@ -155,6 +161,7 @@ def test_project_create_domain(self):
155161
'domain': identity_fakes.domain_id,
156162
'description': None,
157163
'enabled': True,
164+
'parent': None,
158165
}
159166
# ProjectManager.create(name=, domain=, description=,
160167
# enabled=, **kwargs)
@@ -183,6 +190,7 @@ def test_project_create_domain_no_perms(self):
183190
('enable', False),
184191
('disable', False),
185192
('name', identity_fakes.project_name),
193+
('parent', None),
186194
]
187195
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
188196
mocker = mock.Mock()
@@ -197,6 +205,7 @@ def test_project_create_domain_no_perms(self):
197205
'domain': identity_fakes.domain_id,
198206
'description': None,
199207
'enabled': True,
208+
'parent': None,
200209
}
201210
self.projects_mock.create.assert_called_with(
202211
**kwargs
@@ -221,6 +230,7 @@ def test_project_create_enable(self):
221230
('enable', True),
222231
('disable', False),
223232
('name', identity_fakes.project_name),
233+
('parent', None),
224234
]
225235
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
226236

@@ -233,6 +243,7 @@ def test_project_create_enable(self):
233243
'domain': None,
234244
'description': None,
235245
'enabled': True,
246+
'parent': None,
236247
}
237248
# ProjectManager.create(name=, domain=, description=,
238249
# enabled=, **kwargs)
@@ -260,6 +271,7 @@ def test_project_create_disable(self):
260271
('enable', False),
261272
('disable', True),
262273
('name', identity_fakes.project_name),
274+
('parent', None),
263275
]
264276
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
265277

@@ -272,6 +284,7 @@ def test_project_create_disable(self):
272284
'domain': None,
273285
'description': None,
274286
'enabled': False,
287+
'parent': None,
275288
}
276289
# ProjectManager.create(name=, domain=,
277290
# description=, enabled=, **kwargs)
@@ -311,6 +324,7 @@ def test_project_create_property(self):
311324
'domain': None,
312325
'description': None,
313326
'enabled': True,
327+
'parent': None,
314328
'fee': 'fi',
315329
'fo': 'fum',
316330
}
@@ -331,6 +345,92 @@ def test_project_create_property(self):
331345
)
332346
self.assertEqual(datalist, data)
333347

348+
def test_project_create_parent(self):
349+
self.projects_mock.get.return_value = fakes.FakeResource(
350+
None,
351+
copy.deepcopy(identity_fakes.PROJECT),
352+
loaded=True,
353+
)
354+
self.projects_mock.create.return_value = fakes.FakeResource(
355+
None,
356+
copy.deepcopy(identity_fakes.PROJECT_WITH_PARENT),
357+
loaded=True,
358+
)
359+
360+
arglist = [
361+
'--domain', identity_fakes.PROJECT_WITH_PARENT['domain_id'],
362+
'--parent', identity_fakes.PROJECT['name'],
363+
identity_fakes.PROJECT_WITH_PARENT['name'],
364+
]
365+
verifylist = [
366+
('domain', identity_fakes.PROJECT_WITH_PARENT['domain_id']),
367+
('parent', identity_fakes.PROJECT['name']),
368+
('enable', False),
369+
('disable', False),
370+
('name', identity_fakes.PROJECT_WITH_PARENT['name']),
371+
]
372+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
373+
374+
columns, data = self.cmd.take_action(parsed_args)
375+
376+
kwargs = {
377+
'name': identity_fakes.PROJECT_WITH_PARENT['name'],
378+
'domain': identity_fakes.PROJECT_WITH_PARENT['domain_id'],
379+
'parent': identity_fakes.PROJECT['id'],
380+
'description': None,
381+
'enabled': True,
382+
}
383+
384+
self.projects_mock.create.assert_called_with(
385+
**kwargs
386+
)
387+
388+
collist = (
389+
'description',
390+
'domain_id',
391+
'enabled',
392+
'id',
393+
'name',
394+
'parent_id',
395+
)
396+
self.assertEqual(columns, collist)
397+
datalist = (
398+
identity_fakes.PROJECT_WITH_PARENT['description'],
399+
identity_fakes.PROJECT_WITH_PARENT['domain_id'],
400+
identity_fakes.PROJECT_WITH_PARENT['enabled'],
401+
identity_fakes.PROJECT_WITH_PARENT['id'],
402+
identity_fakes.PROJECT_WITH_PARENT['name'],
403+
identity_fakes.PROJECT['id'],
404+
)
405+
self.assertEqual(data, datalist)
406+
407+
def test_project_create_invalid_parent(self):
408+
self.projects_mock.resource_class.__name__ = 'Project'
409+
self.projects_mock.get.side_effect = exceptions.NotFound(
410+
'Invalid parent')
411+
self.projects_mock.find.side_effect = exceptions.NotFound(
412+
'Invalid parent')
413+
414+
arglist = [
415+
'--domain', identity_fakes.domain_name,
416+
'--parent', 'invalid',
417+
identity_fakes.project_name,
418+
]
419+
verifylist = [
420+
('domain', identity_fakes.domain_name),
421+
('parent', 'invalid'),
422+
('enable', False),
423+
('disable', False),
424+
('name', identity_fakes.project_name),
425+
]
426+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
427+
428+
self.assertRaises(
429+
exceptions.CommandError,
430+
self.cmd.take_action,
431+
parsed_args,
432+
)
433+
334434

335435
class TestProjectDelete(TestProject):
336436

0 commit comments

Comments
 (0)