diff --git a/osc_lib/cli/identity.py b/osc_lib/cli/identity.py new file mode 100644 index 0000000..e352371 --- /dev/null +++ b/osc_lib/cli/identity.py @@ -0,0 +1,72 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from openstack import exceptions +from openstack.identity.v3 import project + +from osc_lib.i18n import _ + + +def add_project_owner_option_to_parser(parser): + """Register project and project domain options. + + :param parser: argparse.Argument parser object. + """ + parser.add_argument( + '--project', + metavar='', + help=_("Owner's project (name or ID)") + ) + parser.add_argument( + '--project-domain', + metavar='', + help=_('Domain the project belongs to (name or ID). ' + 'This can be used in case collisions between project names ' + 'exist.'), + ) + + +def find_project(sdk_connection, name_or_id, domain_name_or_id=None): + """Find a project by its name name or ID. + + If Forbidden to find the resource (a common case if the user does not have + permission), then return the resource by creating a local instance of + openstack.identity.v3.Project resource. + + :param sdk_connection: Connection object of OpenStack SDK. + :type sdk_connection: `openstack.connection.Connection` + :param name_or_id: Name or ID of the project + :type name_or_id: string + :param domain_name_or_id: Domain name or ID of the project. + This can be used when there are multiple projects with a same name. + :returns: the project object found + :rtype: `openstack.identity.v3.project.Project` + + """ + try: + if domain_name_or_id: + domain = sdk_connection.identity.find_domain(domain_name_or_id, + ignore_missing=False) + domain_id = domain.id + else: + domain_id = None + return sdk_connection.identity.find_project(name_or_id, + ignore_missing=False, + domain_id=domain_id) + # NOTE: OpenStack SDK raises HttpException for 403 response code. + # There is no specific exception class at now, so we need to catch + # HttpException and check the status code. + except exceptions.HttpException as e: + if e.status_code == 403: + return project.Project(id=name_or_id, name=name_or_id) + raise diff --git a/osc_lib/tests/cli/test_identity.py b/osc_lib/tests/cli/test_identity.py new file mode 100644 index 0000000..c1de255 --- /dev/null +++ b/osc_lib/tests/cli/test_identity.py @@ -0,0 +1,83 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse + +import mock +from openstack import exceptions +from openstack.identity.v3 import project +import testtools + +from osc_lib.cli import identity as cli_identity +from osc_lib.tests import utils as test_utils + + +class IdentityUtilsTestCase(test_utils.TestCase): + + def test_add_project_owner_option_to_parser(self): + parser = argparse.ArgumentParser() + cli_identity.add_project_owner_option_to_parser(parser) + parsed_args = parser.parse_args(['--project', 'project1', + '--project-domain', 'domain1']) + self.assertEqual('project1', parsed_args.project) + self.assertEqual('domain1', parsed_args.project_domain) + + def test_find_project(self): + sdk_connection = mock.Mock() + sdk_find_project = sdk_connection.identity.find_project + sdk_find_project.return_value = mock.sentinel.project1 + + ret = cli_identity.find_project(sdk_connection, 'project1') + self.assertEqual(mock.sentinel.project1, ret) + sdk_find_project.assert_called_once_with( + 'project1', ignore_missing=False, domain_id=None) + + def test_find_project_with_domain(self): + domain1 = mock.Mock() + domain1.id = 'id-domain1' + + sdk_connection = mock.Mock() + sdk_find_domain = sdk_connection.identity.find_domain + sdk_find_domain.return_value = domain1 + sdk_find_project = sdk_connection.identity.find_project + sdk_find_project.return_value = mock.sentinel.project1 + + ret = cli_identity.find_project(sdk_connection, 'project1', 'domain1') + self.assertEqual(mock.sentinel.project1, ret) + sdk_find_domain.assert_called_once_with( + 'domain1', ignore_missing=False) + sdk_find_project.assert_called_once_with( + 'project1', ignore_missing=False, domain_id='id-domain1') + + def test_find_project_with_forbidden_exception(self): + sdk_connection = mock.Mock() + sdk_find_project = sdk_connection.identity.find_project + exc = exceptions.HttpException() + exc.status_code = 403 + sdk_find_project.side_effect = exc + + ret = cli_identity.find_project(sdk_connection, 'project1') + + self.assertIsInstance(ret, project.Project) + self.assertEqual('project1', ret.id) + self.assertEqual('project1', ret.name) + + def test_find_project_with_generic_exception(self): + sdk_connection = mock.Mock() + sdk_find_project = sdk_connection.identity.find_project + exc = exceptions.HttpException() + # Some value other than 403. + exc.status_code = 499 + sdk_find_project.side_effect = exc + + with testtools.ExpectedException(exceptions.HttpException): + cli_identity.find_project(sdk_connection, 'project1') diff --git a/releasenotes/notes/find-project-203bf867619c557e.yaml b/releasenotes/notes/find-project-203bf867619c557e.yaml new file mode 100644 index 0000000..34fd9f4 --- /dev/null +++ b/releasenotes/notes/find-project-203bf867619c557e.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds ``osc_lib.cli.identity.find_project()``. This function can be + used to look up a project ID from command-line options like: + + .. code-block:: python + + find_project(self.app.client_manager.sdk_connection, + parsed_args.project, parsed_args.project_domain) + - | + Adds ``osc_lib.cli.identity.add_project_owner_option_to_parser()`` + to register project and project domain options to CLI.