From 2ed0e220490da5c6ee8f34754d70540890b17921 Mon Sep 17 00:00:00 2001
From: Rodrigo Duarte <rodrigods@lsd.ufcg.edu.br>
Date: Mon, 22 Sep 2014 14:04:01 +0000
Subject: [PATCH] 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
---
 doc/source/command-objects/project.rst        |   6 ++
 openstackclient/identity/v3/project.py        |  15 ++-
 openstackclient/tests/identity/v3/fakes.py    |  10 ++
 .../tests/identity/v3/test_project.py         | 100 ++++++++++++++++++
 4 files changed, 129 insertions(+), 2 deletions(-)

diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst
index 422e239c72..b8607f0b62 100644
--- a/doc/source/command-objects/project.rst
+++ b/doc/source/command-objects/project.rst
@@ -25,6 +25,12 @@ Create new project
 
     .. versionadded:: 3
 
+.. option:: --parent <project>
+
+    Parent of the project (name or ID)
+
+    .. versionadded:: 3
+
 .. option:: --description <description>
 
     Project description
diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py
index 1c93ad5d6c..49979763c0 100644
--- a/openstackclient/identity/v3/project.py
+++ b/openstackclient/identity/v3/project.py
@@ -46,6 +46,11 @@ class CreateProject(show.ShowOne):
             metavar='<domain>',
             help='Domain owning the project (name or ID)',
         )
+        parser.add_argument(
+            '--parent',
+            metavar='<project>',
+            help='Parent of the project (name or ID)',
+        )
         parser.add_argument(
             '--description',
             metavar='<description>',
@@ -86,6 +91,13 @@ class CreateProject(show.ShowOne):
         else:
             domain = None
 
+        parent = None
+        if parsed_args.parent:
+            parent = utils.find_resource(
+                identity_client.projects,
+                parsed_args.parent,
+            ).id
+
         enabled = True
         if parsed_args.disable:
             enabled = False
@@ -97,6 +109,7 @@ class CreateProject(show.ShowOne):
             project = identity_client.projects.create(
                 name=parsed_args.name,
                 domain=domain,
+                parent=parent,
                 description=parsed_args.description,
                 enabled=enabled,
                 **kwargs
@@ -111,8 +124,6 @@ class CreateProject(show.ShowOne):
                 raise e
 
         project._info.pop('links')
-        # TODO(stevemar): Remove the line below when we support multitenancy
-        project._info.pop('parent_id', None)
         return zip(*sorted(six.iteritems(project._info)))
 
 
diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py
index c868401aa1..4056db46cf 100644
--- a/openstackclient/tests/identity/v3/fakes.py
+++ b/openstackclient/tests/identity/v3/fakes.py
@@ -135,6 +135,16 @@ REGION = {
     'links': base_url + 'regions/' + region_id,
 }
 
+PROJECT_WITH_PARENT = {
+    'id': project_id + '-with-parent',
+    'name': project_name + ' and their parents',
+    'description': project_description + ' plus another four',
+    'enabled': True,
+    'domain_id': domain_id,
+    'parent_id': project_id,
+    'links': base_url + 'projects/' + (project_id + '-with-parent'),
+}
+
 role_id = 'r1'
 role_name = 'roller'
 
diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py
index 874908daab..ec50da0c30 100644
--- a/openstackclient/tests/identity/v3/test_project.py
+++ b/openstackclient/tests/identity/v3/test_project.py
@@ -16,6 +16,7 @@
 import copy
 import mock
 
+from openstackclient.common import exceptions
 from openstackclient.identity.v3 import project
 from openstackclient.tests import fakes
 from openstackclient.tests.identity.v3 import fakes as identity_fakes
@@ -60,6 +61,7 @@ class TestProjectCreate(TestProject):
             identity_fakes.project_name,
         ]
         verifylist = [
+            ('parent', None),
             ('enable', False),
             ('disable', False),
             ('name', identity_fakes.project_name),
@@ -75,6 +77,7 @@ class TestProjectCreate(TestProject):
             'domain': None,
             'description': None,
             'enabled': True,
+            'parent': None,
         }
         # ProjectManager.create(name=, domain=, description=,
         #                       enabled=, **kwargs)
@@ -103,6 +106,7 @@ class TestProjectCreate(TestProject):
             ('enable', False),
             ('disable', False),
             ('name', identity_fakes.project_name),
+            ('parent', None),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
@@ -115,6 +119,7 @@ class TestProjectCreate(TestProject):
             'domain': None,
             'description': 'new desc',
             'enabled': True,
+            'parent': None,
         }
         # ProjectManager.create(name=, domain=, description=,
         #                       enabled=, **kwargs)
@@ -143,6 +148,7 @@ class TestProjectCreate(TestProject):
             ('enable', False),
             ('disable', False),
             ('name', identity_fakes.project_name),
+            ('parent', None),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
@@ -155,6 +161,7 @@ class TestProjectCreate(TestProject):
             'domain': identity_fakes.domain_id,
             'description': None,
             'enabled': True,
+            'parent': None,
         }
         # ProjectManager.create(name=, domain=, description=,
         #                       enabled=, **kwargs)
@@ -183,6 +190,7 @@ class TestProjectCreate(TestProject):
             ('enable', False),
             ('disable', False),
             ('name', identity_fakes.project_name),
+            ('parent', None),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         mocker = mock.Mock()
@@ -197,6 +205,7 @@ class TestProjectCreate(TestProject):
             'domain': identity_fakes.domain_id,
             'description': None,
             'enabled': True,
+            'parent': None,
         }
         self.projects_mock.create.assert_called_with(
             **kwargs
@@ -221,6 +230,7 @@ class TestProjectCreate(TestProject):
             ('enable', True),
             ('disable', False),
             ('name', identity_fakes.project_name),
+            ('parent', None),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
@@ -233,6 +243,7 @@ class TestProjectCreate(TestProject):
             'domain': None,
             'description': None,
             'enabled': True,
+            'parent': None,
         }
         # ProjectManager.create(name=, domain=, description=,
         #                       enabled=, **kwargs)
@@ -260,6 +271,7 @@ class TestProjectCreate(TestProject):
             ('enable', False),
             ('disable', True),
             ('name', identity_fakes.project_name),
+            ('parent', None),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
@@ -272,6 +284,7 @@ class TestProjectCreate(TestProject):
             'domain': None,
             'description': None,
             'enabled': False,
+            'parent': None,
         }
         # ProjectManager.create(name=, domain=,
         #                       description=, enabled=, **kwargs)
@@ -311,6 +324,7 @@ class TestProjectCreate(TestProject):
             'domain': None,
             'description': None,
             'enabled': True,
+            'parent': None,
             'fee': 'fi',
             'fo': 'fum',
         }
@@ -331,6 +345,92 @@ class TestProjectCreate(TestProject):
         )
         self.assertEqual(datalist, data)
 
+    def test_project_create_parent(self):
+        self.projects_mock.get.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.PROJECT),
+            loaded=True,
+        )
+        self.projects_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.PROJECT_WITH_PARENT),
+            loaded=True,
+        )
+
+        arglist = [
+            '--domain', identity_fakes.PROJECT_WITH_PARENT['domain_id'],
+            '--parent', identity_fakes.PROJECT['name'],
+            identity_fakes.PROJECT_WITH_PARENT['name'],
+        ]
+        verifylist = [
+            ('domain', identity_fakes.PROJECT_WITH_PARENT['domain_id']),
+            ('parent', identity_fakes.PROJECT['name']),
+            ('enable', False),
+            ('disable', False),
+            ('name', identity_fakes.PROJECT_WITH_PARENT['name']),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {
+            'name': identity_fakes.PROJECT_WITH_PARENT['name'],
+            'domain': identity_fakes.PROJECT_WITH_PARENT['domain_id'],
+            'parent': identity_fakes.PROJECT['id'],
+            'description': None,
+            'enabled': True,
+        }
+
+        self.projects_mock.create.assert_called_with(
+            **kwargs
+        )
+
+        collist = (
+            'description',
+            'domain_id',
+            'enabled',
+            'id',
+            'name',
+            'parent_id',
+        )
+        self.assertEqual(columns, collist)
+        datalist = (
+            identity_fakes.PROJECT_WITH_PARENT['description'],
+            identity_fakes.PROJECT_WITH_PARENT['domain_id'],
+            identity_fakes.PROJECT_WITH_PARENT['enabled'],
+            identity_fakes.PROJECT_WITH_PARENT['id'],
+            identity_fakes.PROJECT_WITH_PARENT['name'],
+            identity_fakes.PROJECT['id'],
+        )
+        self.assertEqual(data, datalist)
+
+    def test_project_create_invalid_parent(self):
+        self.projects_mock.resource_class.__name__ = 'Project'
+        self.projects_mock.get.side_effect = exceptions.NotFound(
+            'Invalid parent')
+        self.projects_mock.find.side_effect = exceptions.NotFound(
+            'Invalid parent')
+
+        arglist = [
+            '--domain', identity_fakes.domain_name,
+            '--parent', 'invalid',
+            identity_fakes.project_name,
+        ]
+        verifylist = [
+            ('domain', identity_fakes.domain_name),
+            ('parent', 'invalid'),
+            ('enable', False),
+            ('disable', False),
+            ('name', identity_fakes.project_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.assertRaises(
+            exceptions.CommandError,
+            self.cmd.take_action,
+            parsed_args,
+        )
+
 
 class TestProjectDelete(TestProject):