diff options
Diffstat (limited to 'openstackclient')
29 files changed, 957 insertions, 57 deletions
diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 184814c6..74c4a46f 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -380,7 +380,7 @@ class APIv1(api.BaseAPI): if len(os.path.dirname(file)) > 0: os.makedirs(os.path.dirname(file)) with open(file, 'wb') as f: - for chunk in response.iter_content(): + for chunk in response.iter_content(64 * 1024): f.write(chunk) def object_set( diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 8526d6bd..ca565d4d 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -117,12 +118,25 @@ class DeleteProject(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for project in parsed_args.projects: - project_obj = utils.find_resource( - identity_client.tenants, - project, - ) - identity_client.tenants.delete(project_obj.id) + try: + project_obj = utils.find_resource( + identity_client.tenants, + project, + ) + identity_client.tenants.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 0a28a70a..e254e05f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -124,12 +124,25 @@ class DeleteRole(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index ddd5b981..2a3dde6b 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -19,6 +19,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -145,12 +146,25 @@ class DeleteUser(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for user in parsed_args.users: - user_obj = utils.find_resource( - identity_client.users, - user, - ) - identity_client.users.delete(user_obj.id) + try: + user_obj = utils.find_resource( + identity_client.users, + user, + ) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index df684c12..a03a86eb 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -20,6 +20,7 @@ import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -194,11 +195,24 @@ class DeleteGroup(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for group in parsed_args.groups: - group_obj = common.find_group(identity_client, - group, - parsed_args.domain) - identity_client.groups.delete(group_obj.id) + try: + group_obj = common.find_group(identity_client, + group, + parsed_args.domain) + identity_client.groups.delete(group_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete group with " + "name or ID '%(group)s': %(e)s"), + {'group': group, 'e': e}) + + if errors > 0: + total = len(parsed_args.groups) + msg = (_("%(errors)s of %(total)s groups failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListGroup(command.Lister): diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index a6348659..12197cdd 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -148,15 +149,28 @@ class DeleteProject(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for project in parsed_args.projects: - if domain is not None: - project_obj = utils.find_resource(identity_client.projects, - project, - domain_id=domain.id) - else: - project_obj = utils.find_resource(identity_client.projects, - project) - identity_client.projects.delete(project_obj.id) + try: + if domain is not None: + project_obj = utils.find_resource(identity_client.projects, + project, + domain_id=domain.id) + else: + project_obj = utils.find_resource(identity_client.projects, + project) + identity_client.projects.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index c9d0fbf3..994ecc9c 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -20,6 +20,7 @@ import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -223,14 +224,26 @@ class DeleteRole(command.Command): if parsed_args.domain: domain_id = common.find_domain(identity_client, parsed_args.domain).id - + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - domain_id=domain_id - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + domain_id=domain_id + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index 62d72ea1..04ee4dce 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -14,8 +14,10 @@ """Identity v3 Trust action implementations""" import datetime +import logging from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -23,6 +25,9 @@ from openstackclient.i18n import _ from openstackclient.identity import common +LOG = logging.getLogger(__name__) + + class CreateTrust(command.ShowOne): _description = _("Create new trust") @@ -145,9 +150,24 @@ class DeleteTrust(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - for t in parsed_args.trust: - trust_obj = utils.find_resource(identity_client.trusts, t) - identity_client.trusts.delete(trust_obj.id) + + errors = 0 + for trust in parsed_args.trust: + try: + trust_obj = utils.find_resource(identity_client.trusts, + trust) + identity_client.trusts.delete(trust_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete trust with " + "name or ID '%(trust)s': %(e)s"), + {'trust': trust, 'e': e}) + + if errors > 0: + total = len(parsed_args.trust) + msg = (_("%(errors)s of %(total)s trusts failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListTrust(command.Lister): diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 796cf28c..19a4c298 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -161,15 +162,28 @@ class DeleteUser(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for user in parsed_args.users: - if domain is not None: - user_obj = utils.find_resource(identity_client.users, - user, - domain_id=domain.id) - else: - user_obj = utils.find_resource(identity_client.users, - user) - identity_client.users.delete(user_obj.id) + try: + if domain is not None: + user_obj = utils.find_resource(identity_client.users, + user, + domain_id=domain.id) + else: + user_obj = utils.find_resource(identity_client.users, + user) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 55eb7eb1..418cc397 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -777,6 +777,23 @@ class SetImage(command.Command): dest=deadopt.replace('-', '_'), help=argparse.SUPPRESS, ) + + membership_group = parser.add_mutually_exclusive_group() + membership_group.add_argument( + "--accept", + action="store_true", + help=_("Accept the image membership"), + ) + membership_group.add_argument( + "--reject", + action="store_true", + help=_("Reject the image membership"), + ) + membership_group.add_argument( + "--pending", + action="store_true", + help=_("Reset the image membership to 'pending'"), + ) return parser def take_action(self, parsed_args): @@ -828,12 +845,14 @@ class SetImage(command.Command): project_arg = parsed_args.owner LOG.warning(_('The --owner option is deprecated, ' 'please use --project instead.')) + project_id = None if project_arg: - kwargs['owner'] = common.find_project( + project_id = common.find_project( identity_client, project_arg, parsed_args.project_domain, ).id + kwargs['owner'] = project_id image = utils.find_resource( image_client.images, parsed_args.image) @@ -846,6 +865,21 @@ class SetImage(command.Command): image_client.images.reactivate(image.id) activation_status = "activated" + membership_group_args = ('accept', 'reject', 'pending') + membership_status = [status for status in membership_group_args + if getattr(parsed_args, status)] + if membership_status: + # If a specific project is not passed, assume we want to update + # our own membership + if not project_id: + project_id = self.app.client_manager.auth_ref.project_id + # The mutually exclusive group of the arg parser ensure we have at + # most one item in the membership_status list. + if membership_status[0] != 'pending': + membership_status[0] += 'ed' # Glance expects the past form + image_client.image_members.update( + image.id, project_id, membership_status[0]) + if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 5420bc8b..df9e320b 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -74,7 +74,7 @@ _formatters_compute = { def _get_columns(item): # Build the display columns and a list of the property columns # that need to be mapped (display column name, property name). - columns = list(item.keys()) + columns = list(item.to_dict().keys()) property_column_mappings = [] if 'security_group_rules' in columns: columns.append('rules') @@ -156,7 +156,7 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne): parsed_args.name, description, ) - display_columns, property_columns = _get_columns(obj._info) + display_columns, property_columns = _get_columns(obj) data = utils.get_dict_properties( obj._info, property_columns, @@ -336,7 +336,7 @@ class ShowSecurityGroup(common.NetworkAndComputeShowOne): client.security_groups, parsed_args.group, ) - display_columns, property_columns = _get_columns(obj._info) + display_columns, property_columns = _get_columns(obj) data = utils.get_dict_properties( obj._info, property_columns, diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 3f432b02..6faff94a 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -74,3 +74,25 @@ class ImageTests(base.TestCase): self.openstack('image unset --property a --property c ' + self.NAME) raw_output = self.openstack('image show ' + self.NAME + opts) self.assertEqual(self.NAME + "\n\n", raw_output) + + def test_image_members(self): + opts = self.get_opts(['project_id']) + my_project_id = self.openstack('token issue' + opts).strip() + self.openstack( + 'image add project {} {}'.format(self.NAME, my_project_id)) + + self.openstack( + 'image set --accept ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared', self.get_opts(['name'])) + ) + self.assertIn(self.NAME, [img['Name'] for img in shared_img_list]) + + self.openstack( + 'image set --reject ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared', self.get_opts(['name'])) + ) + + self.openstack( + 'image remove project {} {}'.format(self.NAME, my_project_id)) diff --git a/openstackclient/tests/functional/network/v2/test_security_group.py b/openstackclient/tests/functional/network/v2/test_security_group.py index 73f6deb3..debd81df 100644 --- a/openstackclient/tests/functional/network/v2/test_security_group.py +++ b/openstackclient/tests/functional/network/v2/test_security_group.py @@ -12,8 +12,6 @@ import uuid -import testtools - from openstackclient.tests.functional import base @@ -25,7 +23,6 @@ class SecurityGroupTests(base.TestCase): FIELDS = ['name'] @classmethod - @testtools.skip('broken SDK testing') def setUpClass(cls): opts = cls.get_opts(cls.FIELDS) raw_output = cls.openstack('security group create ' + cls.NAME + opts) diff --git a/openstackclient/tests/functional/network/v2/test_security_group_rule.py b/openstackclient/tests/functional/network/v2/test_security_group_rule.py index 5d2e5843..ec3731eb 100644 --- a/openstackclient/tests/functional/network/v2/test_security_group_rule.py +++ b/openstackclient/tests/functional/network/v2/test_security_group_rule.py @@ -26,7 +26,6 @@ class SecurityGroupRuleTests(base.TestCase): ID_HEADER = ['ID'] @classmethod - @testtools.skip('broken SDK testing') def setUpClass(cls): # Create the security group to hold the rule. opts = cls.get_opts(cls.NAME_FIELD) @@ -55,6 +54,7 @@ class SecurityGroupRuleTests(base.TestCase): cls.SECURITY_GROUP_NAME) cls.assertOutput('', raw_output) + @testtools.skip('broken SDK testing') def test_security_group_rule_list(self): opts = self.get_opts(self.ID_HEADER) raw_output = self.openstack('security group rule list ' + diff --git a/openstackclient/tests/functional/post_test_hook_tips.sh b/openstackclient/tests/functional/post_test_hook_tips.sh new file mode 100755 index 00000000..994142d8 --- /dev/null +++ b/openstackclient/tests/functional/post_test_hook_tips.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# This is a script that kicks off a series of functional tests against an +# OpenStack cloud. It will attempt to create an instance if one is not +# available. Do not run this script unless you know what you're doing. +# For more information refer to: +# http://docs.openstack.org/developer/python-openstackclient/ + +# This particular script differs from the normal post_test_hook because +# it installs the master (tip) version of osc-lib and openstacksdk + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export OPENSTACKCLIENT_DIR="$BASE/new/python-openstackclient" +sudo chown -R jenkins:stack $OPENSTACKCLIENT_DIR + +# Go to the openstackclient dir +cd $OPENSTACKCLIENT_DIR + +# Run tests +echo "Running openstackclient functional test suite" +set +e + +# Source environment variables to kick things off +source ~stack/devstack/openrc admin admin +echo 'Running tests with:' +env | grep OS + +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -e functional-tips +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index 0732a1ec..626b466d 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -212,6 +212,9 @@ class FakeResource(object): def keys(self): return self._info.keys() + def to_dict(self): + return self._info + @property def info(self): return self._info diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index c1f00762..4e1077db 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -13,8 +13,11 @@ # under the License. # +import mock + from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import project from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -302,6 +305,32 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_project, + exceptions.CommandError] + arglist = [ + self.fake_project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.fake_project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.fake_project.id) + class TestProjectList(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 68ebf141..684ce803 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import role from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -240,6 +241,32 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_role, + exceptions.CommandError] + arglist = [ + self.fake_role.id, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, self.fake_role.id) + find_mock.assert_any_call(self.roles_mock, 'unexist_role') + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(self.fake_role.id) + class TestRoleList(TestRole): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 765f8559..a8b9497e 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import user from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -411,6 +412,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_user, + exceptions.CommandError] + arglist = [ + self.fake_user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.fake_user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.fake_user.id) + class TestUserList(TestUser): diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index eb50adb5..8558de95 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -16,6 +16,7 @@ from mock import call from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import group from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -257,6 +258,32 @@ class TestGroupDelete(TestGroup): self.groups_mock.delete.assert_called_once_with(self.groups[0].id) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_groups_with_exception(self, find_mock): + find_mock.side_effect = [self.groups[0], + exceptions.CommandError] + arglist = [ + self.groups[0].id, + 'unexist_group', + ] + verifylist = [ + ('groups', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.groups_mock, self.groups[0].id) + find_mock.assert_any_call(self.groups_mock, 'unexist_group') + + self.assertEqual(2, find_mock.call_count) + self.groups_mock.delete.assert_called_once_with(self.groups[0].id) + class TestGroupList(TestGroup): diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 702d9209..2b898090 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -16,6 +16,7 @@ import mock from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import project from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -445,6 +446,32 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.project, + exceptions.CommandError] + arglist = [ + self.project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.project.id) + class TestProjectList(TestProject): diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 448e18d3..c0b68bdf 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -14,6 +14,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import role from openstackclient.tests.unit import fakes @@ -428,6 +432,36 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.roles_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.role_name, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, + identity_fakes.role_name, + domain_id=None) + find_mock.assert_any_call(self.roles_mock, + 'unexist_role', + domain_id=None) + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(identity_fakes.role_id) + class TestRoleList(TestRole): diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 4eeb8bfe..93e8f63d 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -12,6 +12,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import trust from openstackclient.tests.unit import fakes @@ -148,6 +152,33 @@ class TestTrustDelete(TestTrust): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_trusts_with_exception(self, find_mock): + find_mock.side_effect = [self.trusts_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.trust_id, + 'unexist_trust', + ] + verifylist = [ + ('trust', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 trusts failed to delete.', + str(e)) + + find_mock.assert_any_call(self.trusts_mock, identity_fakes.trust_id) + find_mock.assert_any_call(self.trusts_mock, 'unexist_trust') + + self.assertEqual(2, find_mock.call_count) + self.trusts_mock.delete.assert_called_once_with( + identity_fakes.trust_id) + class TestTrustList(TestTrust): diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 6150a5f3..3c1f49a6 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -16,6 +16,9 @@ import contextlib import mock +from osc_lib import exceptions +from osc_lib import utils + from openstackclient.identity.v3 import user from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -465,6 +468,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.user, + exceptions.CommandError] + arglist = [ + self.user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.user.id) + class TestUserList(TestUser): diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index a054e513..a1513119 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -845,6 +845,39 @@ class TestImageSet(TestImage): self.assertIsNone(result) + self.image_members_mock.update.assert_not_called() + + def test_image_set_membership_option(self): + membership = image_fakes.FakeImage.create_one_image_member( + attrs={'image_id': image_fakes.image_id, + 'member_id': self.project.id} + ) + self.image_members_mock.update.return_value = membership + + for status in ('accept', 'reject', 'pending'): + arglist = [ + '--%s' % status, + image_fakes.image_id, + ] + verifylist = [ + (status, True), + ('image', image_fakes.image_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_members_mock.update.assert_called_once_with( + image_fakes.image_id, + self.app.client_manager.auth_ref.project_id, + status if status == 'pending' else status + 'ed' + ) + self.image_members_mock.update.reset_mock() + + # Assert that the 'update image" route is also called, in addition to + # the 'update membership' route. + self.images_mock.update.assert_called_with(image_fakes.image_id) + def test_image_set_options(self): arglist = [ '--name', 'new-name', diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index bc99ca8d..6eeeae39 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -36,10 +36,115 @@ class TestConsistencyGroup(volume_fakes.TestVolume): self.app.client_manager.volume.cgsnapshots) self.cgsnapshots_mock.reset_mock() + self.volumes_mock = ( + self.app.client_manager.volume.volumes) + self.volumes_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() +class TestConsistencyGroupAddVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupAddVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.AddVolumeToConsistencyGroup(self.app, None) + + def test_add_one_volume_to_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_add_multiple_volumes_to_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_add_multiple_volumes_to_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to add.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, add_volumes=volume.id + ) + + class TestConsistencyGroupCreate(TestConsistencyGroup): volume_type = volume_fakes.FakeType.create_one_type() @@ -394,6 +499,107 @@ class TestConsistencyGroupList(TestConsistencyGroup): self.assertEqual(self.data_long, list(data)) +class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupRemoveVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.RemoveVolumeFromConsistencyGroup(self.app, None) + + def test_remove_one_volume_from_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_remove_multi_volumes_from_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_remove_multiple_volumes_from_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to remove.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, remove_volumes=volume.id + ) + + class TestConsistencyGroupSet(TestConsistencyGroup): consistency_group = ( diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 1529c2e2..4fef9dd9 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -823,6 +823,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -853,6 +866,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -885,6 +911,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -915,6 +954,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] device = self.mock_volume.attachments[0]['device'] @@ -946,6 +998,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -976,6 +1041,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': self.mock_volume.name, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1006,6 +1084,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': self.mock_volume.status, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1036,6 +1127,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1067,6 +1171,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + collist = [ 'ID', 'Display Name', diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 2f4f3c95..0a932f84 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -27,6 +27,60 @@ from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _find_volumes(parsed_args_volumes, volume_client): + result = 0 + uuid = '' + for volume in parsed_args_volumes: + try: + volume_id = utils.find_resource( + volume_client.volumes, volume).id + uuid += volume_id + ',' + except Exception as e: + result += 1 + LOG.error(_("Failed to find volume with " + "name or ID '%(volume)s':%(e)s") + % {'volume': volume, 'e': e}) + + return result, uuid + + +class AddVolumeToConsistencyGroup(command.Command): + _description = _("Add volume(s) to consistency group") + + def get_parser(self, prog_name): + parser = super(AddVolumeToConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group to contain <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to add to <consistency-group> (name or ID) ' + '(repeat option to add multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, add_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to add.") % {'result': result, 'total': total}) + + if add_uuid: + add_uuid = add_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, add_volumes=add_uuid) + + class CreateConsistencyGroup(command.ShowOne): _description = _("Create new consistency group.") @@ -188,6 +242,44 @@ class ListConsistencyGroup(command.Lister): for s in consistency_groups)) +class RemoveVolumeFromConsistencyGroup(command.Command): + _description = _("Remove volume(s) from consistency group") + + def get_parser(self, prog_name): + parser = \ + super(RemoveVolumeFromConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group containing <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to remove from <consistency-group> (name or ID) ' + '(repeat option to remove multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to remove.") % {'result': result, 'total': total}) + + if remove_uuid: + remove_uuid = remove_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, remove_volumes=remove_uuid) + + class SetConsistencyGroup(command.Command): _description = _("Set consistency group properties") diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 301bf5e4..78db261b 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -417,16 +417,19 @@ class ListVolume(command.Lister): project_id = identity_common.find_project( identity_client, parsed_args.project, - parsed_args.project_domain) + parsed_args.project_domain).id user_id = None if parsed_args.user: user_id = identity_common.find_user(identity_client, parsed_args.user, - parsed_args.user_domain) + parsed_args.user_domain).id + + # set value of 'all_tenants' when using project option + all_projects = bool(parsed_args.project) or parsed_args.all_projects search_opts = { - 'all_tenants': parsed_args.all_projects, + 'all_tenants': all_projects, 'project_id': project_id, 'user_id': user_id, 'display_name': parsed_args.name, |
